summaryrefslogtreecommitdiffstats
path: root/coffin/template/library.py
diff options
context:
space:
mode:
Diffstat (limited to 'coffin/template/library.py')
-rw-r--r--coffin/template/library.py215
1 files changed, 215 insertions, 0 deletions
diff --git a/coffin/template/library.py b/coffin/template/library.py
new file mode 100644
index 00000000..8e80edc5
--- /dev/null
+++ b/coffin/template/library.py
@@ -0,0 +1,215 @@
+from django.template import Library as DjangoLibrary, InvalidTemplateLibrary
+from jinja2.ext import Extension as Jinja2Extension
+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_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.filters = copy(django_library.filters)
+ result.tags = copy(django_library.tags)
+ for name, func in result.filters.iteritems():
+ result._register_filter(name, func, jinja2_only=True)
+ 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):
+ """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 isinstance(name_or_node, Jinja2Extension):
+ if compile_function:
+ raise InvalidTemplateLibrary('"compile_function" argument not supported for Jinja2 extensions')
+ self.jinja2_extensions.append(name_or_node)
+ return name_or_node
+ else:
+ return super(Library, self).tag(name_or_node, compile_function)
+
+ def tag_function(self, func_or_node):
+ if 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, jinja2_only=False):
+ """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.
+
+ 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, 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, jinja2_only=jinja2_only)
+ return dec
+ elif name != None and filter_func != None:
+ # register.filter('somename', somefunc)
+ return self._register_filter(name, filter_func,
+ jinja2_only=jinja2_only)
+ else:
+ raise InvalidTemplateLibrary("Unsupported arguments to "
+ "Library.filter: (%r, %r)", (name, filter_func))
+
+ def _register_filter(self, name, func, jinja2_only=None):
+ filter_type, can_be_ported = guess_filter_type(func)
+ if filter_type == JINJA2 and not can_be_ported:
+ self.jinja2_filters[name] = func
+ return func
+ elif filter_type == DJANGO and not can_be_ported:
+ if jinja2_only:
+ raise ValueError('This filter cannot be ported to Jinja2.')
+ self.filters[name] = func
+ return func
+ elif jinja2_only:
+ func = django_filter_to_jinja2(func)
+ self.jinja2_filters[name] = func
+ return func
+ else:
+ # register the filter with both engines
+ django_func = jinja2_filter_to_django(func)
+ jinja2_func = django_filter_to_jinja2(func)
+ self.filters[name] = django_func
+ self.jinja2_filters[name] = jinja2_func
+ return (django_func, jinja2_func)