Skip to content

Commit

Permalink
added Django/Jinja 2 compatibility layer and several bugfixes
Browse files Browse the repository at this point in the history
  • Loading branch information
rick committed Oct 24, 2010
1 parent 2f205b3 commit 3e7af03
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 114 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
Christopher D. Leary <[email protected]>
Michael Elsdoerfer <[email protected]>
David Cramer <[email protected]>
Rick van Hattem <[email protected]>
11 changes: 4 additions & 7 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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()``.
Expand Down
23 changes: 19 additions & 4 deletions coffin/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,19 @@

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)
Expand Down Expand Up @@ -70,14 +77,16 @@ def _get_templatelibs(self):
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
Expand All @@ -103,7 +112,7 @@ def _get_all_extensions(self):
# 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):
Expand All @@ -113,10 +122,16 @@ def from_setting(setting):
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
Expand Down
151 changes: 134 additions & 17 deletions coffin/template/__init__.py
Original file line number Diff line number Diff line change
@@ -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__
Expand All @@ -12,8 +18,81 @@
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
Expand All @@ -22,38 +101,74 @@ 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
# be parse ourself and iterate over the AST?
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:
Expand All @@ -69,7 +184,7 @@ def dict_from_django_context(context):


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.
Expand All @@ -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')

13 changes: 10 additions & 3 deletions coffin/template/defaultfilters.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@

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):
Expand Down Expand Up @@ -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)

Loading

0 comments on commit 3e7af03

Please sign in to comment.