deb-python-coffin/coffin/interop.py

128 lines
4.6 KiB
Python

"""Compatibility functions between Jinja2 and Django.
General notes:
- The Django ``stringfilter`` decorator is supported, but should not be
used when writing filters specifically for Jinja: It will lose the
attributes attached to the filter function by Jinja's
``environmentfilter`` and ``contextfilter`` decorators, when used
in the wrong order.
Maybe coffin should provide a custom version of stringfilter.
- While transparently converting filters between Django and Jinja works
for the most part, there is an issue with Django's
``mark_for_escaping``, as Jinja does not support a similar mechanism.
Instead, for Jinja, we escape such strings immediately (whereas Django
defers it to the template engine).
"""
import inspect
from django.utils.safestring import SafeUnicode, SafeData, EscapeData
from jinja2 import Markup, environmentfilter, Undefined
__all__ = (
'DJANGO', 'JINJA2',
'django_filter_to_jinja2',
'jinja2_filter_to_django',
'guess_filter_type',)
DJANGO = 'django'
JINJA2 = 'jinja2'
def django_filter_to_jinja2(filter_func):
"""
Note: Due to the way this function is used by
``coffin.template.Library``, it needs to be able to handle native
Jinja2 filters and pass them through unmodified. This necessity
stems from the fact that it is not always possible to determine
the type of a filter.
TODO: Django's "func.is_safe" is not yet handled
"""
def _convert_out(v):
if isinstance(v, SafeData):
return Markup(v)
if isinstance(v, EscapeData):
return Markup.escape(v) # not 100% equivalent, see mod docs
return v
def _convert_in(v):
if isinstance(v, Undefined):
# Essentially the TEMPLATE_STRING_IF_INVALID default
# setting. If a non-default is set, Django wouldn't apply
# filters. This is something that we neither can nor want to
# simulate in Jinja.
return ''
return v
def conversion_wrapper(value, *args, **kwargs):
result = filter_func(_convert_in(value), *args, **kwargs)
return _convert_out(result)
# Jinja2 supports a similar machanism to Django's
# ``needs_autoescape`` filters: environment filters. We can
# thus support Django filters that use it in Jinja2 with just
# a little bit of argument rewriting.
if hasattr(filter_func, 'needs_autoescape'):
@environmentfilter
def autoescape_wrapper(environment, *args, **kwargs):
kwargs['autoescape'] = environment.autoescape
return conversion_wrapper(*args, **kwargs)
return autoescape_wrapper
else:
return conversion_wrapper
def jinja2_filter_to_django(filter_func):
"""
Note: Due to the way this function is used by
``coffin.template.Library``, it needs to be able to handle native
Django filters and pass them through unmodified. This necessity
stems from the fact that it is not always possible to determine
the type of a filter.
"""
if guess_filter_type(filter_func)[0] == DJANGO:
return filter_func
def _convert(v):
# TODO: for now, this is not even necessary: Markup strings have
# a custom replace() method that is immume to Django's escape()
# attempts.
#if isinstance(v, Markup):
# return SafeUnicode(v) # jinja is always unicode
# ... Jinja does not have a EscapeData equivalent
return v
def wrapped(value, *args, **kwargs):
result = filter_func(value, *args, **kwargs)
return _convert(result)
return wrapped
def guess_filter_type(filter_func):
"""Returns a 2-tuple of (type, can_be_ported).
``type`` is one of DJANGO, JINJA2, or ``False`` if the type can
not be determined.
``can_be_ported`` is ``True`` if we believe the filter could be
ported to the other engine, respectively, or ``False`` if we know
it can't.
TODO: May not yet use all possible clues, e.g. decorators like
``stringfilter``.
TOOD: Needs tests.
"""
if hasattr(filter_func, 'contextfilter') or \
hasattr(filter_func, 'environmentfilter'):
return JINJA2, False
args = inspect.getargspec(filter_func)
if len(args[0]) - (len(args[3]) if args[3] else 0) > 2:
return JINJA2, False
if hasattr(filter_func, 'needs_autoescape'):
return DJANGO, True
# Looks like your run of the mill Python function, which are
# easily convertible in either direction.
return False, True