253 lines
10 KiB
Python
253 lines
10 KiB
Python
from django.template import Library as DjangoLibrary, InvalidTemplateLibrary
|
|
from jinja2.ext import Extension as Jinja2Extension
|
|
import types
|
|
from coffin.interop import (
|
|
DJANGO, JINJA2,
|
|
guess_filter_type, jinja2_filter_to_django, django_filter_to_jinja2)
|
|
|
|
|
|
__all__ = ['Library']
|
|
|
|
|
|
class Library(DjangoLibrary):
|
|
"""Version of the Django ``Library`` class that can handle both
|
|
Django template engine tags and filters, as well as Jinja2
|
|
extensions and filters.
|
|
|
|
Tries to present a common registration interface to the extension
|
|
author, but provides both template engines with only those
|
|
components they can support.
|
|
|
|
Since custom Django tags and Jinja2 extensions are two completely
|
|
different beasts, they are handled completely separately. You can
|
|
register custom Django tags as usual, for example:
|
|
|
|
register.tag('current_time', do_current_time)
|
|
|
|
Or register a Jinja2 extension like this:
|
|
|
|
register.tag(CurrentTimeNode)
|
|
|
|
Filters, on the other hand, work similarily in both engines, and
|
|
for the most one can't tell whether a filter function was written
|
|
for Django or Jinja2. A compatibility layer is used to make to
|
|
make the filters you register usuable with both engines:
|
|
|
|
register.filter('cut', cut)
|
|
|
|
However, some of the more powerful filters just won't work in
|
|
Django, for example if more than one argument is required, or if
|
|
context- or environmentfilters are used. If ``cut`` in the above
|
|
example where such an extended filter, it would only be registered
|
|
with Jinja.
|
|
|
|
See also the module documentation for ``coffin.interop`` for
|
|
information on some of the limitations of this conversion.
|
|
|
|
TODO: Jinja versions of the ``simple_tag`` and ``inclusion_tag``
|
|
helpers would be nice, though since custom tags are not needed as
|
|
often in Jinja, this is not urgent.
|
|
"""
|
|
|
|
def __init__(self):
|
|
super(Library, self).__init__()
|
|
self.jinja2_filters = {}
|
|
self.jinja2_extensions = []
|
|
self.jinja2_environment_attrs = {}
|
|
self.jinja2_globals = {}
|
|
self.jinja2_tests = {}
|
|
|
|
@classmethod
|
|
def from_django(cls, django_library):
|
|
"""Create a Coffin library object from a Django library.
|
|
|
|
Specifically, this ensures that filters already registered
|
|
with the Django library are also made available to Jinja,
|
|
where applicable.
|
|
"""
|
|
from copy import copy
|
|
result = cls()
|
|
result.tags = copy(django_library.tags)
|
|
for name, func in django_library.filters.iteritems():
|
|
result._register_filter(name, func, type='django')
|
|
return result
|
|
|
|
def test(self, name=None, func=None):
|
|
def inner(f):
|
|
name = getattr(f, "_decorated_function", f).__name__
|
|
self.jinja2_tests[name] = f
|
|
return f
|
|
if name == None and func == None:
|
|
# @register.test()
|
|
return inner
|
|
elif func == None:
|
|
if (callable(name)):
|
|
# register.test()
|
|
return inner(name)
|
|
else:
|
|
# @register.test('somename') or @register.test(name='somename')
|
|
def dec(func):
|
|
return self.test(name, func)
|
|
return dec
|
|
elif name != None and func != None:
|
|
# register.filter('somename', somefunc)
|
|
self.jinja2_tests[name] = func
|
|
return func
|
|
else:
|
|
raise InvalidTemplateLibrary("Unsupported arguments to "
|
|
"Library.test: (%r, %r)", (name, func))
|
|
|
|
def object(self, name=None, func=None):
|
|
def inner(f):
|
|
name = getattr(f, "_decorated_function", f).__name__
|
|
self.jinja2_globals[name] = f
|
|
return f
|
|
if name == None and func == None:
|
|
# @register.object()
|
|
return inner
|
|
elif func == None:
|
|
if (callable(name)):
|
|
# register.object()
|
|
return inner(name)
|
|
else:
|
|
# @register.object('somename') or @register.object(name='somename')
|
|
def dec(func):
|
|
return self.object(name, func)
|
|
return dec
|
|
elif name != None and func != None:
|
|
# register.object('somename', somefunc)
|
|
self.jinja2_globals[name] = func
|
|
return func
|
|
else:
|
|
raise InvalidTemplateLibrary("Unsupported arguments to "
|
|
"Library.object: (%r, %r)", (name, func))
|
|
|
|
def tag(self, name_or_node=None, compile_function=None, environment={}):
|
|
"""Register a Django template tag (1) or Jinja 2 extension (2).
|
|
|
|
For (1), supports the same invocation syntax as the original
|
|
Django version, including use as a decorator.
|
|
|
|
For (2), since Jinja 2 extensions are classes (which can't be
|
|
decorated), and have the tag name effectively built in, only the
|
|
following syntax is supported:
|
|
|
|
register.tag(MyJinjaExtensionNode)
|
|
|
|
If your extension needs to be configured by setting environment
|
|
attributes, you can can pass key-value pairs via ``environment``.
|
|
"""
|
|
if isinstance(name_or_node, type) and issubclass(name_or_node, Jinja2Extension):
|
|
if compile_function:
|
|
raise InvalidTemplateLibrary('"compile_function" argument not supported for Jinja2 extensions')
|
|
self.jinja2_extensions.append(name_or_node)
|
|
self.jinja2_environment_attrs.update(environment)
|
|
return name_or_node
|
|
else:
|
|
if environment:
|
|
raise InvalidTemplateLibrary('"environment" argument not supported for Django tags')
|
|
return super(Library, self).tag(name_or_node, compile_function)
|
|
|
|
def tag_function(self, func_or_node):
|
|
if not isinstance(func_or_node, types.FunctionType) and \
|
|
issubclass(func_or_node, Jinja2Extension):
|
|
self.jinja2_extensions.append(func_or_node)
|
|
return func_or_node
|
|
else:
|
|
return super(Library, self).tag_function(func_or_node)
|
|
|
|
def filter(self, name=None, filter_func=None, type=None, jinja2_only=None):
|
|
"""Register a filter with both the Django and Jinja2 template
|
|
engines, if possible - or only Jinja2, if ``jinja2_only`` is
|
|
specified. ``jinja2_only`` does not affect conversion of the
|
|
filter if neccessary.
|
|
|
|
Implements a compatibility layer to handle the different
|
|
auto-escaping approaches transparently. Extended Jinja2 filter
|
|
features like environment- and contextfilters are however not
|
|
supported in Django. Such filters will only be registered with
|
|
Jinja.
|
|
|
|
If you know which template language the filter was written for,
|
|
you may want to specify type="django" or type="jinja2", to disable
|
|
the interop layer which in some cases might not be able to operate
|
|
entirely opaque. For example, Jinja 2 filters may not receive a
|
|
"Undefined" value if the interop layer is applied.
|
|
|
|
Supports the same invocation syntax as the original Django
|
|
version, including use as a decorator.
|
|
|
|
If the function is supposed to return the registered filter
|
|
(by example of the superclass implementation), but has
|
|
registered multiple filters, a tuple of all filters is
|
|
returned.
|
|
"""
|
|
def filter_function(f):
|
|
return self._register_filter(
|
|
getattr(f, "_decorated_function", f).__name__,
|
|
f, type=type, jinja2_only=jinja2_only)
|
|
if name == None and filter_func == None:
|
|
# @register.filter()
|
|
return filter_function
|
|
elif filter_func == None:
|
|
if (callable(name)):
|
|
# @register.filter
|
|
return filter_function(name)
|
|
else:
|
|
# @register.filter('somename') or @register.filter(name='somename')
|
|
def dec(func):
|
|
return self.filter(name, func, type=type,
|
|
jinja2_only=jinja2_only)
|
|
return dec
|
|
elif name != None and filter_func != None:
|
|
# register.filter('somename', somefunc)
|
|
return self._register_filter(name, filter_func, type=type,
|
|
jinja2_only=jinja2_only)
|
|
else:
|
|
raise InvalidTemplateLibrary("Unsupported arguments to "
|
|
"Library.filter: (%r, %r)", (name, filter_func))
|
|
|
|
def jinja2_filter(self, *args, **kwargs):
|
|
"""Shortcut for filter(type='jinja2').
|
|
"""
|
|
kw = {'type': JINJA2}
|
|
kw.update(kwargs)
|
|
return self.filter(*args, **kw)
|
|
|
|
def _register_filter(self, name, func, type=None, jinja2_only=None):
|
|
assert type in (None, JINJA2, DJANGO,)
|
|
|
|
# The user might not specify the language the filter was written
|
|
# for, but sometimes we can auto detect it.
|
|
filter_type, can_be_ported = guess_filter_type(func)
|
|
assert not (filter_type and type) or filter_type == type, \
|
|
"guessed filter type (%s) not matching claimed type (%s)" % (
|
|
filter_type, type,
|
|
)
|
|
if not filter_type and type:
|
|
filter_type = type
|
|
|
|
if filter_type == JINJA2:
|
|
self.jinja2_filters[name] = func
|
|
if can_be_ported and not jinja2_only:
|
|
self.filters[name] = jinja2_filter_to_django(func)
|
|
return func
|
|
elif filter_type == DJANGO:
|
|
self.filters[name] = func
|
|
if not can_be_ported and jinja2_only:
|
|
raise ValueError('This filter cannot be ported to Jinja2.')
|
|
if can_be_ported:
|
|
self.jinja2_filters[name] = django_filter_to_jinja2(func)
|
|
return func
|
|
else:
|
|
django_func = jinja2_filter_to_django(func)
|
|
jinja2_func = django_filter_to_jinja2(func)
|
|
if jinja2_only:
|
|
self.jinja2_filters[name] = jinja2_func
|
|
return jinja2_func
|
|
else:
|
|
# register the filter with both engines
|
|
self.filters[name] = django_func
|
|
self.jinja2_filters[name] = jinja2_func
|
|
return (django_func, jinja2_func)
|