added Django/Jinja 2 compatibility layer and several bugfixes

This commit is contained in:
rick 2010-10-24 02:52:02 +02:00
parent 2f205b38f7
commit 3e7af037c9
8 changed files with 257 additions and 114 deletions

View File

@ -1,3 +1,4 @@
Christopher D. Leary <cdleary@gmail.com> Christopher D. Leary <cdleary@gmail.com>
Michael Elsdoerfer <michael@elsdoerfer.info> Michael Elsdoerfer <michael@elsdoerfer.info>
David Cramer <dcramer@gmail.com> David Cramer <dcramer@gmail.com>
Rick van Hattem <rick@fawo.nl>

View File

@ -133,13 +133,10 @@ templates anyway, it might be a good opportunity for this change.
(*) http://groups.google.com/group/django-developers/browse_thread/thread/f323338045ac2e5e (*) http://groups.google.com/group/django-developers/browse_thread/thread/f323338045ac2e5e
Jinja2's ``TemplateSyntaxError`` (and potentially other exception types) This version of coffin modifies Jinja 2's ``TemplateSyntaxError`` to be
are not compatible with Django's own template exceptions with respect to compatible with Django. So there is no need to disable ``TEMPLATE_DEBUG``.
the TEMPLATE_DEBUG facility. If TEMPLATE_DEBUG is enabled and Jinja2 raises You can just keep `TEPMLATE_DEBUG=True`` in your settings to benefit from both
an exception, Django's error 500 page will sometimes not be able to handle Jinja 2 and Django's template debugging.
it and crash. The solution is to disable the TEMPLATE_DEBUG setting in
Django. See http://code.djangoproject.com/ticket/10216 for further
information.
``coffin.template.loader`` is a port of ``django.template.loader`` and ``coffin.template.loader`` is a port of ``django.template.loader`` and
comes with a Jinja2-enabled version of ``get_template()``. comes with a Jinja2-enabled version of ``get_template()``.

View File

@ -12,12 +12,19 @@ _JINJA_I18N_EXTENSION_NAME = 'jinja2.ext.i18n'
class CoffinEnvironment(Environment): class CoffinEnvironment(Environment):
def __init__(self, filters={}, globals={}, tests={}, loader=None, extensions=[], **kwargs): def __init__(self, filters={}, globals={}, tests={}, loader=None, extensions=[], **kwargs):
from django.conf import settings
if not loader: if not loader:
loader = loaders.ChoiceLoader(self._get_loaders()) loader = loaders.ChoiceLoader(self._get_loaders())
all_ext = self._get_all_extensions() all_ext = self._get_all_extensions()
extensions.extend(all_ext['extensions']) extensions.extend(all_ext['extensions'])
super(CoffinEnvironment, self).__init__(extensions=extensions, loader=loader, **kwargs) super(CoffinEnvironment, self).__init__(
extensions=extensions,
loader=loader,
cache_size=-1,
auto_reload=settings.DEBUG,
**kwargs
)
self.filters.update(filters) self.filters.update(filters)
self.filters.update(all_ext['filters']) self.filters.update(all_ext['filters'])
self.globals.update(globals) self.globals.update(globals)
@ -70,14 +77,16 @@ class CoffinEnvironment(Environment):
pass pass
else: else:
for f in os.listdir(path): for f in os.listdir(path):
if f == '__init__.py': if f == '__init__.py' or f.startswith('.'):
continue continue
if f.endswith('.py'): if f.endswith('.py'):
try: try:
# TODO: will need updating when #6587 lands # TODO: will need updating when #6587 lands
# libs.append(get_library( # libs.append(get_library(
# "django.templatetags.%s" % os.path.splitext(f)[0])) # "django.templatetags.%s" % os.path.splitext(f)[0]))
libs.append(get_library(os.path.splitext(f)[0])) library = os.path.splitext(f)[0]
libs.append(get_library(library))
except InvalidTemplateLibrary: except InvalidTemplateLibrary:
pass pass
@ -103,7 +112,7 @@ class CoffinEnvironment(Environment):
# add the globally defined extension list # add the globally defined extension list
extensions.extend(list(getattr(settings, 'JINJA2_EXTENSIONS', []))) extensions.extend(list(getattr(settings, 'JINJA2_EXTENSIONS', [])))
def from_setting(setting): def from_setting(setting, call=False):
retval = {} retval = {}
setting = getattr(settings, setting, {}) setting = getattr(settings, setting, {})
if isinstance(setting, dict): if isinstance(setting, dict):
@ -113,10 +122,16 @@ class CoffinEnvironment(Environment):
for value in setting: for value in setting:
value = callable(value) and value or get_callable(value) value = callable(value) and value or get_callable(value)
retval[value.__name__] = value retval[value.__name__] = value
if call:
for k, v in retval.items():
if callable(v):
retval[k] = v()
return retval return retval
filters.update(from_setting('JINJA2_FILTERS')) filters.update(from_setting('JINJA2_FILTERS'))
globals.update(from_setting('JINJA2_GLOBALS')) globals.update(from_setting('JINJA2_GLOBALS'))
globals.update(from_setting('JINJA2_CONSTANTS', True))
tests.update(from_setting('JINJA2_TESTS')) tests.update(from_setting('JINJA2_TESTS'))
# add extensions defined in application's templatetag libraries # add extensions defined in application's templatetag libraries

View File

@ -1,8 +1,14 @@
from jinja2 import (
exceptions as _jinja2_exceptions,
environment as _jinja2_environment,
)
from django.template import ( from django.template import (
Context as DjangoContext, Context as DjangoContext,
add_to_builtins as django_add_to_builtins, add_to_builtins as django_add_to_builtins,
import_library) import_library,
from jinja2 import Template as _Jinja2Template TemplateSyntaxError as DjangoTemplateSyntaxError,
loader as django_loader,
)
# Merge with ``django.template``. # Merge with ``django.template``.
from django.template import __all__ from django.template import __all__
@ -12,8 +18,81 @@ from django.template import *
from library import * from library import *
class Template(_Jinja2Template): def _generate_django_exception(e, source=None):
"""Fixes the incompabilites between Jinja2's template class and '''Generate a Django exception from a Jinja exception'''
from django.views.debug import linebreak_iter
import re
if source:
exception = DjangoTemplateSyntaxError(e.message)
exception_dict = e.__dict__
del exception_dict['source']
# Fetch the entire template in a string
template_string = source[0].reload()
# Get the line number from the error message, if available
match = re.match('.* at (\d+)$', e.message)
start_index = 0
stop_index = 0
if match:
# Convert the position found in the stacktrace to a position
# the Django template debug system can use
position = int(match.group(1)) + source[1][0] + 1
for index in linebreak_iter(template_string):
if index >= position:
stop_index = min(index, position + 3)
start_index = min(index, position - 2)
break
start_index = index
else:
# So there wasn't a matching error message, in that case we
# simply have to highlight the entire line instead of the specific
# words
ignore_lines = -1
for i, index in enumerate(linebreak_iter(template_string)):
if source[1][0] > index:
ignore_lines += 1
if i - ignore_lines == e.lineno:
stop_index = index
break
start_index = index
# Convert the positions to a source that is compatible with the
# Django template debugger
source = source[0], (
start_index,
stop_index,
)
else:
# No source available so we let Django fetch it for us
lineno = e.lineno - 1
template_string, source = django_loader.find_template_source(e.name)
exception = DjangoTemplateSyntaxError(e.message)
# Find the positions by the line number given in the exception
start_index = 0
for i in range(lineno):
start_index = template_string.index('\n', start_index + 1)
source = source, (
start_index + 1,
template_string.index('\n', start_index + 1) + 1,
)
# Set our custom source as source for the exception so the Django
# template debugger can use it
exception.source = source
return exception
class Template(_jinja2_environment.Template):
'''Fixes the incompabilites between Jinja2's template class and
Django's. Django's.
The end result should be a class that renders Jinja2 templates but The end result should be a class that renders Jinja2 templates but
@ -22,15 +101,24 @@ class Template(_Jinja2Template):
This includes flattening a ``Context`` instance passed to render This includes flattening a ``Context`` instance passed to render
and making sure that this class will automatically use the global and making sure that this class will automatically use the global
coffin environment. coffin environment.
""" '''
def __new__(cls, template_string, origin=None, name=None): def __new__(cls, template_string, origin=None, name=None, source=None):
# We accept the "origin" and "name" arguments, but discard them # We accept the "origin" and "name" arguments, but discard them
# right away - Jinja's Template class (apparently) stores no # right away - Jinja's Template class (apparently) stores no
# equivalent information. # equivalent information.
# source is expected to be a Django Template Loader source, it is not
# required but helps to provide useful stacktraces when executing
# Jinja code from Django templates
from coffin.common import env from coffin.common import env
return env.from_string(template_string, template_class=cls) try:
template = env.from_string(template_string, template_class=cls)
template.source = source
return template
except _jinja2_exceptions.TemplateSyntaxError, e:
raise _generate_django_exception(e, source)
def __iter__(self): def __iter__(self):
# TODO: Django allows iterating over the templates nodes. Should # TODO: Django allows iterating over the templates nodes. Should
@ -38,22 +126,49 @@ class Template(_Jinja2Template):
raise NotImplementedError() raise NotImplementedError()
def render(self, context=None): def render(self, context=None):
"""Differs from Django's own render() slightly in that makes the '''Differs from Django's own render() slightly in that makes the
``context`` parameter optional. We try to strike a middle ground ``context`` parameter optional. We try to strike a middle ground
here between implementing Django's interface while still supporting here between implementing Django's interface while still supporting
Jinja's own call syntax as well. Jinja's own call syntax as well.
""" '''
if context is None: if not context:
context = {} context = {}
else: else:
context = dict_from_django_context(context) context = dict_from_django_context(context)
assert isinstance(context, dict) # Required for **-operator.
return super(Template, self).render(**context) try:
return super(Template, self).render(context)
except _jinja2_exceptions.TemplateSyntaxError, e:
raise _generate_django_exception(e)
except _jinja2_exceptions.UndefinedError, e:
# UndefinedErrors don't have a source attribute so we create one
import sys
import traceback
exc_traceback = sys.exc_info()[-1]
trace = traceback.extract_tb(exc_traceback)[-1]
e.lineno = trace[1]
source = None
# If we're getting <template> than we're being call from a memory
# template, this occurs when we use the {% jinja %} template tag
# In that case we use the Django source and find our position
# within that
if trace[0] == '<template>' and hasattr(self, 'source'):
source = self.source
e.name = source[0].name
e.source = source
else:
e.name = trace[0]
# We have to cleanup the trace manually, Python does _not_ clean
# it up for us!
del exc_traceback, trace
raise _generate_django_exception(e, source)
def dict_from_django_context(context): def dict_from_django_context(context):
"""Flattens a Django :class:`django.template.context.Context` object. '''Flattens a Django :class:`django.template.context.Context` object.'''
"""
if not isinstance(context, DjangoContext): if not isinstance(context, DjangoContext):
return context return context
else: else:
@ -69,7 +184,7 @@ builtins = []
def add_to_builtins(module_name): def add_to_builtins(module_name):
"""Add the given module to both Coffin's list of default template '''Add the given module to both Coffin's list of default template
libraries as well as Django's. This makes sense, since Coffin libraries as well as Django's. This makes sense, since Coffin
libs are compatible with Django libraries. libs are compatible with Django libraries.
@ -84,10 +199,12 @@ def add_to_builtins(module_name):
XXX/TODO: Why do we need our own custom list of builtins? Our XXX/TODO: Why do we need our own custom list of builtins? Our
Library object is compatible, remember!? We can just add them Library object is compatible, remember!? We can just add them
directly to Django's own list of builtins. directly to Django's own list of builtins.
""" '''
builtins.append(import_library(module_name)) builtins.append(import_library(module_name))
django_add_to_builtins(module_name) django_add_to_builtins(module_name)
add_to_builtins('coffin.template.defaulttags') add_to_builtins('coffin.template.defaulttags')
add_to_builtins('coffin.template.defaultfilters') add_to_builtins('coffin.template.defaultfilters')
add_to_builtins('coffin.template.interop')

View File

@ -5,15 +5,17 @@ TODO: Most of the filters in here need to be updated for autoescaping.
from coffin.template import Library from coffin.template import Library
from jinja2.runtime import Undefined from jinja2.runtime import Undefined
# from jinja2 import Markup from jinja2 import filters
register = Library() register = Library()
@register.filter(jinja2_only=True)
def url(view_name, *args, **kwargs): def url(view_name, *args, **kwargs):
from coffin.template.defaulttags import url from coffin.template.defaulttags import url
return url._reverse(view_name, args, kwargs) return url._reverse(view_name, args, kwargs)
register.filter(url, jinja2_only=True)
register.object(url)
@register.filter(jinja2_only=True) @register.filter(jinja2_only=True)
def timesince(value, *arg): def timesince(value, *arg):
if value is None or isinstance(value, Undefined): if value is None or isinstance(value, Undefined):
@ -94,4 +96,9 @@ def floatformat(value, arg=-1):
result = django_filter_to_jinja2(floatformat)(value, arg) result = django_filter_to_jinja2(floatformat)(value, arg)
if result == '': # django couldn't handle the value if result == '': # django couldn't handle the value
raise ValueError(value) raise ValueError(value)
return result return result
@register.filter(jinja2_only=True)
def default(value, default_value=u'', boolean=True):
return filters.do_default(value, default_value, boolean)

View File

@ -86,7 +86,8 @@ class LoadExtension(Extension):
# #
# TODO: Actually, there's ``nodes.EnvironmentAttribute``. # TODO: Actually, there's ``nodes.EnvironmentAttribute``.
#ae_setting = object.__new__(nodes.InternalName) #ae_setting = object.__new__(nodes.InternalName)
#nodes.Node.__init__(ae_setting, 'environment.autoescape', lineno=lineno) #nodes.Node.__init__(ae_setting, 'environment.autoescape',
lineno=lineno)
#temp = parser.free_identifier() #temp = parser.free_identifier()
#body.insert(0, nodes.Assign(temp, ae_setting)) #body.insert(0, nodes.Assign(temp, ae_setting))
#body.insert(1, nodes.Assign(ae_setting, nodes.Const(True))) #body.insert(1, nodes.Assign(ae_setting, nodes.Const(True)))
@ -133,7 +134,7 @@ class URLExtension(Extension):
bits = [] bits = []
name_allowed = True name_allowed = True
while True: while True:
if stream.current.test_any('dot', 'sub'): if stream.current.test_any('dot', 'sub', 'colon'):
bits.append(stream.next()) bits.append(stream.next())
name_allowed = True name_allowed = True
elif stream.current.test('name') and name_allowed: elif stream.current.test('name') and name_allowed:
@ -160,15 +161,18 @@ class URLExtension(Extension):
else: else:
args.append(parser.parse_expression()) args.append(parser.parse_expression())
make_call_node = lambda *kw: \ def make_call_node(*kw):
self.call_method('_reverse', return self.call_method('_reverse', args=[
args=[viewname, nodes.List(args), nodes.Dict(kwargs)], viewname,
kwargs=kw) nodes.List(args),
nodes.Dict(kwargs),
], kwargs=kw)
# if an as-clause is specified, write the result to context... # if an as-clause is specified, write the result to context...
if stream.next_if('name:as'): if stream.next_if('name:as'):
var = nodes.Name(stream.expect('name').value, 'store') var = nodes.Name(stream.expect('name').value, 'store')
call_node = make_call_node(nodes.Keyword('fail', nodes.Const(False))) call_node = make_call_node(nodes.Keyword('fail',
nodes.Const(False)))
return nodes.Assign(var, call_node) return nodes.Assign(var, call_node)
# ...otherwise print it out. # ...otherwise print it out.
else: else:
@ -223,13 +227,13 @@ class WithExtension(Extension):
parser.stream.expect('name:as') parser.stream.expect('name:as')
name = parser.stream.expect('name') name = parser.stream.expect('name')
body = parser.parse_statements(['name:endwith'], drop_needle=True) body = parser.parse_statements(['name:endwith'], drop_needle=True)
# Use a local variable instead of a macro argument to alias # Use a local variable instead of a macro argument to alias
# the expression. This allows us to nest "with" statements. # the expression. This allows us to nest "with" statements.
body.insert(0, nodes.Assign(nodes.Name(name.value, 'store'), value)) body.insert(0, nodes.Assign(nodes.Name(name.value, 'store'), value))
return nodes.CallBlock( return nodes.CallBlock(
self.call_method('_render_block'), [], [], body).\ self.call_method('_render_block'), [], [], body).\
set_lineno(lineno) set_lineno(lineno)
def _render_block(self, caller=None): def _render_block(self, caller=None):
return caller() return caller()
@ -312,12 +316,12 @@ class CacheExtension(Extension):
from django.core.cache import cache # delay depending in settings from django.core.cache import cache # delay depending in settings
from django.utils.http import urlquote from django.utils.http import urlquote
from django.utils.hashcompat import md5_constructor from django.utils.hashcompat import md5_constructor
try: try:
expire_time = int(expire_time) expire_time = int(expire_time)
except (ValueError, TypeError): except (ValueError, TypeError):
raise TemplateSyntaxError('"%s" tag got a non-integer ' raise TemplateSyntaxError('"%s" tag got a non-integer timeout '
'timeout value: %r' % (list(self.tags)[0], expire_time), lineno) 'value: %r' % (list(self.tags)[0], expire_time), lineno)
args_string = u':'.join([urlquote(v) for v in vary_on]) args_string = u':'.join([urlquote(v) for v in vary_on])
args_md5 = md5_constructor(args_string) args_md5 = md5_constructor(args_string)
@ -343,7 +347,7 @@ class SpacelessExtension(Extension):
body = parser.parse_statements(['name:endspaceless'], drop_needle=True) body = parser.parse_statements(['name:endspaceless'], drop_needle=True)
return nodes.CallBlock( return nodes.CallBlock(
self.call_method('_strip_spaces', [], [], None, None), self.call_method('_strip_spaces', [], [], None, None),
[], [], body [], [], body,
).set_lineno(lineno) ).set_lineno(lineno)
def _strip_spaces(self, caller=None): def _strip_spaces(self, caller=None):
@ -366,7 +370,7 @@ class CsrfTokenExtension(Extension):
def parse(self, parser): def parse(self, parser):
lineno = parser.stream.next().lineno lineno = parser.stream.next().lineno
return nodes.Output([ return nodes.Output([
self.call_method('_render', [nodes.Name('csrf_token', 'load')]) self.call_method('_render', [nodes.Name('csrf_token', 'load')]),
]).set_lineno(lineno) ]).set_lineno(lineno)
def _render(self, csrf_token): def _render(self, csrf_token):
@ -382,11 +386,11 @@ cache = CacheExtension
spaceless = SpacelessExtension spaceless = SpacelessExtension
csrf_token = CsrfTokenExtension csrf_token = CsrfTokenExtension
register = Library() register = Library()
register.tag(load) register.tag(load)
register.tag(url) register.tag(url)
register.tag(with_) register.tag(with_)
register.tag(cache) register.tag(cache)
register.tag(spaceless) register.tag(spaceless)
register.tag(csrf_token) register.tag(csrf_token)

View File

@ -1,5 +1,6 @@
from django.template import Library as DjangoLibrary, InvalidTemplateLibrary from django.template import Library as DjangoLibrary, InvalidTemplateLibrary
from jinja2.ext import Extension as Jinja2Extension from jinja2.ext import Extension as Jinja2Extension
import types
from coffin.interop import ( from coffin.interop import (
DJANGO, JINJA2, DJANGO, JINJA2,
guess_filter_type, jinja2_filter_to_django, django_filter_to_jinja2) guess_filter_type, jinja2_filter_to_django, django_filter_to_jinja2)
@ -142,7 +143,8 @@ class Library(DjangoLibrary):
return super(Library, self).tag(name_or_node, compile_function) return super(Library, self).tag(name_or_node, compile_function)
def tag_function(self, func_or_node): def tag_function(self, func_or_node):
if issubclass(func_or_node, Jinja2Extension): if not isinstance(func_or_node, types.FunctionType) and \
issubclass(func_or_node, Jinja2Extension):
self.jinja2_extensions.append(func_or_node) self.jinja2_extensions.append(func_or_node)
return func_or_node return func_or_node
else: else:

View File

@ -1,66 +1,66 @@
"""Replacement for ``django.template.loader`` that uses Jinja 2. """Replacement for ``django.template.loader`` that uses Jinja 2.
The module provides a generic way to load templates from an arbitrary The module provides a generic way to load templates from an arbitrary
backend storage (e.g. filesystem, database). backend storage (e.g. filesystem, database).
""" """
from coffin.template import Template as CoffinTemplate from coffin.template import Template as CoffinTemplate
from jinja2 import TemplateNotFound from jinja2 import TemplateNotFound
def find_template_source(name, dirs=None): def find_template_source(name, dirs=None):
# This is Django's most basic loading function through which # This is Django's most basic loading function through which
# all template retrievals go. Not sure if Jinja 2 publishes # all template retrievals go. Not sure if Jinja 2 publishes
# an equivalent, but no matter, it mostly for internal use # an equivalent, but no matter, it mostly for internal use
# anyway - developers will want to start with # anyway - developers will want to start with
# ``get_template()`` or ``get_template_from_string`` anyway. # ``get_template()`` or ``get_template_from_string`` anyway.
raise NotImplementedError() raise NotImplementedError()
def get_template(template_name): def get_template(template_name):
# Jinja will handle this for us, and env also initializes # Jinja will handle this for us, and env also initializes
# the loader backends the first time it is called. # the loader backends the first time it is called.
from coffin.common import env from coffin.common import env
return env.get_template(template_name) return env.get_template(template_name)
def get_template_from_string(source): def get_template_from_string(source):
""" """
Does not support then ``name`` and ``origin`` parameters from Does not support then ``name`` and ``origin`` parameters from
the Django version. the Django version.
""" """
from coffin.common import env from coffin.common import env
return env.from_string(source) return env.from_string(source)
def render_to_string(template_name, dictionary=None, context_instance=None): def render_to_string(template_name, dictionary=None, context_instance=None):
"""Loads the given ``template_name`` and renders it with the given """Loads the given ``template_name`` and renders it with the given
dictionary as context. The ``template_name`` may be a string to load dictionary as context. The ``template_name`` may be a string to load
a single template using ``get_template``, or it may be a tuple to use a single template using ``get_template``, or it may be a tuple to use
``select_template`` to find one of the templates in the list. ``select_template`` to find one of the templates in the list.
``dictionary`` may also be Django ``Context`` object. ``dictionary`` may also be Django ``Context`` object.
Returns a string. Returns a string.
""" """
dictionary = dictionary or {} dictionary = dictionary or {}
if isinstance(template_name, (list, tuple)): if isinstance(template_name, (list, tuple)):
template = select_template(template_name) template = select_template(template_name)
else: else:
template = get_template(template_name) template = get_template(template_name)
if context_instance: if context_instance:
context_instance.update(dictionary) context_instance.update(dictionary)
else: else:
context_instance = dictionary context_instance = dictionary
return template.render(context_instance) return template.render(context_instance)
def select_template(template_name_list): def select_template(template_name_list):
"Given a list of template names, returns the first that can be loaded." "Given a list of template names, returns the first that can be loaded."
for template_name in template_name_list: for template_name in template_name_list:
try: try:
return get_template(template_name) return get_template(template_name)
except TemplateNotFound: except TemplateNotFound:
continue continue
# If we get here, none of the templates could be loaded # If we get here, none of the templates could be loaded
raise TemplateNotFound(', '.join(template_name_list)) raise TemplateNotFound(', '.join(template_name_list))