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>
Michael Elsdoerfer <michael@elsdoerfer.info>
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
Jinja2's ``TemplateSyntaxError`` (and potentially other exception types)
are not compatible with Django's own template exceptions with respect to
the TEMPLATE_DEBUG facility. If TEMPLATE_DEBUG is enabled and Jinja2 raises
an exception, Django's error 500 page will sometimes not be able to handle
it and crash. The solution is to disable the TEMPLATE_DEBUG setting in
Django. See http://code.djangoproject.com/ticket/10216 for further
information.
This version of coffin modifies Jinja 2's ``TemplateSyntaxError`` to be
compatible with Django. So there is no need to disable ``TEMPLATE_DEBUG``.
You can just keep `TEPMLATE_DEBUG=True`` in your settings to benefit from both
Jinja 2 and Django's template debugging.
``coffin.template.loader`` is a port of ``django.template.loader`` and
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):
def __init__(self, filters={}, globals={}, tests={}, loader=None, extensions=[], **kwargs):
from django.conf import settings
if not loader:
loader = loaders.ChoiceLoader(self._get_loaders())
all_ext = self._get_all_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(all_ext['filters'])
self.globals.update(globals)
@ -70,14 +77,16 @@ class CoffinEnvironment(Environment):
pass
else:
for f in os.listdir(path):
if f == '__init__.py':
if f == '__init__.py' or f.startswith('.'):
continue
if f.endswith('.py'):
try:
# TODO: will need updating when #6587 lands
# libs.append(get_library(
# "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:
pass
@ -103,7 +112,7 @@ class CoffinEnvironment(Environment):
# add the globally defined extension list
extensions.extend(list(getattr(settings, 'JINJA2_EXTENSIONS', [])))
def from_setting(setting):
def from_setting(setting, call=False):
retval = {}
setting = getattr(settings, setting, {})
if isinstance(setting, dict):
@ -113,10 +122,16 @@ class CoffinEnvironment(Environment):
for value in setting:
value = callable(value) and value or get_callable(value)
retval[value.__name__] = value
if call:
for k, v in retval.items():
if callable(v):
retval[k] = v()
return retval
filters.update(from_setting('JINJA2_FILTERS'))
globals.update(from_setting('JINJA2_GLOBALS'))
globals.update(from_setting('JINJA2_CONSTANTS', True))
tests.update(from_setting('JINJA2_TESTS'))
# 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 (
Context as DjangoContext,
add_to_builtins as django_add_to_builtins,
import_library)
from jinja2 import Template as _Jinja2Template
import_library,
TemplateSyntaxError as DjangoTemplateSyntaxError,
loader as django_loader,
)
# Merge with ``django.template``.
from django.template import __all__
@ -12,8 +18,81 @@ from django.template import *
from library import *
class Template(_Jinja2Template):
"""Fixes the incompabilites between Jinja2's template class and
def _generate_django_exception(e, source=None):
'''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.
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
and making sure that this class will automatically use the global
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
# right away - Jinja's Template class (apparently) stores no
# 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
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):
# TODO: Django allows iterating over the templates nodes. Should
@ -38,22 +126,49 @@ class Template(_Jinja2Template):
raise NotImplementedError()
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
here between implementing Django's interface while still supporting
Jinja's own call syntax as well.
"""
if context is None:
'''
if not context:
context = {}
else:
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):
"""Flattens a Django :class:`django.template.context.Context` object.
"""
'''Flattens a Django :class:`django.template.context.Context` object.'''
if not isinstance(context, DjangoContext):
return context
else:
@ -69,7 +184,7 @@ builtins = []
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
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
Library object is compatible, remember!? We can just add them
directly to Django's own list of builtins.
"""
'''
builtins.append(import_library(module_name))
django_add_to_builtins(module_name)
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 jinja2.runtime import Undefined
# from jinja2 import Markup
from jinja2 import filters
register = Library()
@register.filter(jinja2_only=True)
def url(view_name, *args, **kwargs):
from coffin.template.defaulttags import url
return url._reverse(view_name, args, kwargs)
register.filter(url, jinja2_only=True)
register.object(url)
@register.filter(jinja2_only=True)
def timesince(value, *arg):
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)
if result == '': # django couldn't handle the 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``.
#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()
#body.insert(0, nodes.Assign(temp, ae_setting))
#body.insert(1, nodes.Assign(ae_setting, nodes.Const(True)))
@ -133,7 +134,7 @@ class URLExtension(Extension):
bits = []
name_allowed = True
while True:
if stream.current.test_any('dot', 'sub'):
if stream.current.test_any('dot', 'sub', 'colon'):
bits.append(stream.next())
name_allowed = True
elif stream.current.test('name') and name_allowed:
@ -160,15 +161,18 @@ class URLExtension(Extension):
else:
args.append(parser.parse_expression())
make_call_node = lambda *kw: \
self.call_method('_reverse',
args=[viewname, nodes.List(args), nodes.Dict(kwargs)],
kwargs=kw)
def make_call_node(*kw):
return self.call_method('_reverse', args=[
viewname,
nodes.List(args),
nodes.Dict(kwargs),
], kwargs=kw)
# if an as-clause is specified, write the result to context...
if stream.next_if('name:as'):
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)
# ...otherwise print it out.
else:
@ -223,13 +227,13 @@ class WithExtension(Extension):
parser.stream.expect('name:as')
name = parser.stream.expect('name')
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.
body.insert(0, nodes.Assign(nodes.Name(name.value, 'store'), value))
return nodes.CallBlock(
self.call_method('_render_block'), [], [], body).\
set_lineno(lineno)
def _render_block(self, caller=None):
return caller()
@ -312,12 +316,12 @@ class CacheExtension(Extension):
from django.core.cache import cache # delay depending in settings
from django.utils.http import urlquote
from django.utils.hashcompat import md5_constructor
try:
expire_time = int(expire_time)
except (ValueError, TypeError):
raise TemplateSyntaxError('"%s" tag got a non-integer '
'timeout value: %r' % (list(self.tags)[0], expire_time), lineno)
raise TemplateSyntaxError('"%s" tag got a non-integer timeout '
'value: %r' % (list(self.tags)[0], expire_time), lineno)
args_string = u':'.join([urlquote(v) for v in vary_on])
args_md5 = md5_constructor(args_string)
@ -343,7 +347,7 @@ class SpacelessExtension(Extension):
body = parser.parse_statements(['name:endspaceless'], drop_needle=True)
return nodes.CallBlock(
self.call_method('_strip_spaces', [], [], None, None),
[], [], body
[], [], body,
).set_lineno(lineno)
def _strip_spaces(self, caller=None):
@ -366,7 +370,7 @@ class CsrfTokenExtension(Extension):
def parse(self, parser):
lineno = parser.stream.next().lineno
return nodes.Output([
self.call_method('_render', [nodes.Name('csrf_token', 'load')])
self.call_method('_render', [nodes.Name('csrf_token', 'load')]),
]).set_lineno(lineno)
def _render(self, csrf_token):
@ -382,11 +386,11 @@ cache = CacheExtension
spaceless = SpacelessExtension
csrf_token = CsrfTokenExtension
register = Library()
register.tag(load)
register.tag(url)
register.tag(with_)
register.tag(cache)
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 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)
@ -142,7 +143,8 @@ class Library(DjangoLibrary):
return super(Library, self).tag(name_or_node, compile_function)
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)
return func_or_node
else:

View File

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