diff options
Diffstat (limited to 'coffin/template/library.py')
-rw-r--r-- | coffin/template/library.py | 215 |
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) |