diff options
Diffstat (limited to 'coffin')
36 files changed, 1309 insertions, 0 deletions
diff --git a/coffin/.___init__.py b/coffin/.___init__.py Binary files differnew file mode 100644 index 00000000..b44b5fae --- /dev/null +++ b/coffin/.___init__.py diff --git a/coffin/._common.py b/coffin/._common.py Binary files differnew file mode 100644 index 00000000..c83780af --- /dev/null +++ b/coffin/._common.py diff --git a/coffin/._interop.py b/coffin/._interop.py Binary files differnew file mode 100644 index 00000000..94121d1f --- /dev/null +++ b/coffin/._interop.py diff --git a/coffin/__init__.py b/coffin/__init__.py new file mode 100644 index 00000000..df89bdd6 --- /dev/null +++ b/coffin/__init__.py @@ -0,0 +1,44 @@ +""" +Coffin +~~~~~~ + +`Coffin <http://www.github.com/dcramer/coffin>` is a package that resolves the +impedance mismatch between `Django <http://www.djangoproject.com/>` and `Jinja2 +<http://jinja.pocoo.org/2/>` through various adapters. The aim is to use Coffin +as a drop-in replacement for Django's template system to whatever extent is +reasonable. + +:copyright: 2008 by Christopher D. Leary +:license: BSD, see LICENSE for more details. +""" + + +__all__ = ('__version__', '__build__', '__docformat__', 'get_revision') +__version__ = (0, 3) +__docformat__ = 'restructuredtext en' + +import os + +def _get_git_revision(path): + revision_file = os.path.join(path, 'refs', 'heads', 'master') + if not os.path.exists(revision_file): + return None + fh = open(revision_file, 'r') + try: + return fh.read() + finally: + fh.close() + +def get_revision(): + """ + :returns: Revision number of this branch/checkout, if available. None if + no revision number can be determined. + """ + package_dir = os.path.dirname(__file__) + checkout_dir = os.path.normpath(os.path.join(package_dir, '..')) + path = os.path.join(checkout_dir, '.git') + if os.path.exists(path): + return _get_git_revision(path) + return None + +__build__ = get_revision() diff --git a/coffin/common.py b/coffin/common.py new file mode 100644 index 00000000..2e381ff3 --- /dev/null +++ b/coffin/common.py @@ -0,0 +1,148 @@ +import os +import warnings + +from django import dispatch +from jinja2 import Environment, loaders + +__all__ = ('env', 'need_env') + +env = None + +_JINJA_I18N_EXTENSION_NAME = 'jinja2.ext.i18n' + +# TODO: This should be documented (as even I'm not sure where it's use-case is) +need_env = dispatch.Signal(providing_args=['arguments', 'loaders', + 'filters', 'extensions', + 'globals', 'tests']) + +class CoffinEnvironment(Environment): + def __init__(self, filters={}, globals={}, tests={}, loader=None, extensions=[], **kwargs): + 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) + self.filters.update(filters) + self.filters.update(all_ext['filters']) + self.globals.update(globals) + self.globals.update(all_ext['globals']) + self.tests.update(tests) + self.tests.update(all_ext['tests']) + + from coffin.template import Template as CoffinTemplate + self.template_class = CoffinTemplate + + def _get_loaders(self): + """Tries to translate each template loader given in the Django settings + (:mod:`django.settings`) to a similarly-behaving Jinja loader. + Warns if a similar loader cannot be found. + Allows for Jinja2 loader instances to be placed in the template loader + settings. + """ + loaders = [] + + from coffin.template.loaders import jinja_loader_from_django_loader + + from django.conf import settings + for loader in settings.TEMPLATE_LOADERS: + if isinstance(loader, basestring): + loader_obj = jinja_loader_from_django_loader(loader) + if loader_obj: + loaders.append(loader_obj) + else: + warnings.warn('Cannot translate loader: %s' % loader) + else: # It's assumed to be a Jinja2 loader instance. + loaders.append(loader) + return loaders + + + def _get_templatelibs(self): + """Return an iterable of template ``Library`` instances. + + Since we cannot support the {% load %} tag in Jinja, we have to + register all libraries globally. + """ + from django.conf import settings + from django.template import get_library, InvalidTemplateLibrary + + libs = [] + for a in settings.INSTALLED_APPS: + try: + path = __import__(a + '.templatetags', {}, {}, ['__file__']).__file__ + path = os.path.dirname(path) # we now have the templatetags/ directory + except ImportError: + pass + else: + for f in os.listdir(path): + if f == '__init__.py': + 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])) + except InvalidTemplateLibrary: + pass + return libs + + def _get_all_extensions(self): + from django.conf import settings + from coffin.template import builtins + from django.core.urlresolvers import get_callable + + extensions, filters, globals, tests = [], {}, {}, {} + + # start with our builtins + for lib in builtins: + extensions.extend(getattr(lib, 'jinja2_extensions', [])) + filters.update(getattr(lib, 'jinja2_filters', {})) + globals.update(getattr(lib, 'jinja2_globals', {})) + tests.update(getattr(lib, 'jinja2_tests', {})) + + if settings.USE_I18N: + extensions.append(_JINJA_I18N_EXTENSION_NAME) + + # add the globally defined extension list + extensions.extend(list(getattr(settings, 'JINJA2_EXTENSIONS', []))) + + def from_setting(setting): + retval = {} + setting = getattr(settings, setting, {}) + if isinstance(setting, dict): + for key, value in setting.iteritems(): + retval[user] = callable(value) and value or get_callable(value) + else: + for value in setting: + value = callable(value) and value or get_callable(value) + retval[value.__name__] = value + return retval + + filters.update(from_setting('JINJA2_FILTERS')) + globals.update(from_setting('JINJA2_GLOBALS')) + tests.update(from_setting('JINJA2_TESTS')) + + # add extensions defined in application's templatetag libraries + for lib in self._get_templatelibs(): + extensions.extend(getattr(lib, 'jinja2_extensions', [])) + filters.update(getattr(lib, 'jinja2_filters', {})) + globals.update(getattr(lib, 'jinja2_globals', {})) + tests.update(getattr(lib, 'jinja2_tests', {})) + + return dict( + extensions=extensions, + filters=filters, + globals=globals, + tests=tests, + ) + +def get_env(): + """ + :return: A Jinja2 environment singleton. + """ + # need_env.send(sender=Environment, arguments=arguments, + # loaders=loaders_, extensions=extensions, + # filters=filters, tests=tests, globals=globals) + return CoffinEnvironment(autoescape=True) + +env = get_env() diff --git a/coffin/conf/.___init__.py b/coffin/conf/.___init__.py Binary files differnew file mode 100644 index 00000000..c6b47018 --- /dev/null +++ b/coffin/conf/.___init__.py diff --git a/coffin/conf/__init__.py b/coffin/conf/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/coffin/conf/__init__.py diff --git a/coffin/conf/urls/__init__.py b/coffin/conf/urls/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/coffin/conf/urls/__init__.py diff --git a/coffin/conf/urls/defaults.py b/coffin/conf/urls/defaults.py new file mode 100644 index 00000000..3049e2b9 --- /dev/null +++ b/coffin/conf/urls/defaults.py @@ -0,0 +1,4 @@ +from django.conf.urls.defaults import * + +handler404 = 'coffin.views.defaults.page_not_found' +handler500 = 'coffin.views.defaults.server_error'
\ No newline at end of file diff --git a/coffin/contrib/__init__.py b/coffin/contrib/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/coffin/contrib/__init__.py diff --git a/coffin/contrib/markup/__init__.py b/coffin/contrib/markup/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/coffin/contrib/markup/__init__.py diff --git a/coffin/contrib/markup/models.py b/coffin/contrib/markup/models.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/coffin/contrib/markup/models.py diff --git a/coffin/contrib/markup/templatetags/__init__.py b/coffin/contrib/markup/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/coffin/contrib/markup/templatetags/__init__.py diff --git a/coffin/contrib/markup/templatetags/markup.py b/coffin/contrib/markup/templatetags/markup.py new file mode 100644 index 00000000..0d6b92f9 --- /dev/null +++ b/coffin/contrib/markup/templatetags/markup.py @@ -0,0 +1,15 @@ +"""Makes the template filters from the ``django.contrib.markup`` app +available to both the Jinja2 and Django engines. + +In other words, adding ``coffin.contrib.markup`` to your INSTALLED_APPS +setting will enable the markup filters not only through Coffin, but +also through the default Django template system. +""" + +from coffin.template import Library as CoffinLibrary +from django.contrib.markup.templatetags.markup import register + + +# Convert Django's Library into a Coffin Library object, which will +# make sure the filters are correctly ported to Jinja2. +register = CoffinLibrary.from_django(register)
\ No newline at end of file diff --git a/coffin/contrib/syndication/__init__.py b/coffin/contrib/syndication/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/coffin/contrib/syndication/__init__.py diff --git a/coffin/contrib/syndication/feeds.py b/coffin/contrib/syndication/feeds.py new file mode 100644 index 00000000..f8f0701f --- /dev/null +++ b/coffin/contrib/syndication/feeds.py @@ -0,0 +1,36 @@ +from django.contrib.syndication.feeds import * # merge modules
+
+import sys
+from django.contrib.syndication.feeds import Feed as DjangoFeed
+from coffin.template import loader as coffin_loader
+
+
+class Feed(DjangoFeed):
+ """A ``Feed`` implementation that renders it's title and
+ description templates using Jinja2.
+
+ Unfortunately, Django's base ``Feed`` class is not very extensible
+ in this respect at all. For a real solution, we'd have to essentially
+ have to duplicate the whole class. So for now, we use this terrible
+ non-thread safe hack.
+
+ Another, somewhat crazy option would be:
+ * Render the templates ourselves through Jinja2 (possible
+ introduce new attributes to avoid having to rewrite the
+ existing ones).
+ * Make the rendered result available to Django/the superclass by
+ using a custom template loader using a prefix, say
+ "feed:<myproject.app.views.MyFeed>". The loader would simply
+ return the Jinja-rendered template (escaped), the Django template
+ mechanism would find no nodes and just pass the output through.
+ Possible even worse than this though.
+ """
+
+ def get_feed(self, *args, **kwargs):
+ parent_module = sys.modules[DjangoFeed.__module__]
+ old_loader = parent_module.loader
+ parent_module.loader = coffin_loader
+ try:
+ return super(Feed, self).get_feed(*args, **kwargs)
+ finally:
+ parent_module.loader = old_loader
\ No newline at end of file diff --git a/coffin/interop.py b/coffin/interop.py new file mode 100644 index 00000000..54120004 --- /dev/null +++ b/coffin/interop.py @@ -0,0 +1,120 @@ +"""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
+
+
+__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(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 conversion_wrapper(*args, **kwargs):
+ result = filter_func(*args, **kwargs)
+ return _convert(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
\ No newline at end of file diff --git a/coffin/shortcuts/.___init__.py b/coffin/shortcuts/.___init__.py Binary files differnew file mode 100644 index 00000000..c21832ed --- /dev/null +++ b/coffin/shortcuts/.___init__.py diff --git a/coffin/shortcuts/__init__.py b/coffin/shortcuts/__init__.py new file mode 100644 index 00000000..c882dfcd --- /dev/null +++ b/coffin/shortcuts/__init__.py @@ -0,0 +1,25 @@ +from django.http import HttpResponse + +# Merge with original namespace so user +# doesn't have to import twice. +from django.shortcuts import * + + +__all__ = ('render_to_string', 'render_to_response',) + + +# Is within ``template.loader`` as per Django specification - +# but I think it fits very well here. +from coffin.template.loader import render_to_string + + +def render_to_response(template_name, dictionary=None, context_instance=None, + mimetype=None): + """ + :param template_name: Filename of the template to get or a sequence of + filenames to try, in order. + :param dictionary: Rendering context for the template. + :returns: A response object with the evaluated template as a payload. + """ + rendered = render_to_string(template_name, dictionary, context_instance) + return HttpResponse(rendered, mimetype=mimetype) diff --git a/coffin/template/.___init__.py b/coffin/template/.___init__.py Binary files differnew file mode 100644 index 00000000..87947898 --- /dev/null +++ b/coffin/template/.___init__.py diff --git a/coffin/template/._defaultfilters.py b/coffin/template/._defaultfilters.py Binary files differnew file mode 100644 index 00000000..8fd7d746 --- /dev/null +++ b/coffin/template/._defaultfilters.py diff --git a/coffin/template/._defaulttags.py b/coffin/template/._defaulttags.py Binary files differnew file mode 100644 index 00000000..db9dcec7 --- /dev/null +++ b/coffin/template/._defaulttags.py diff --git a/coffin/template/._library.py b/coffin/template/._library.py Binary files differnew file mode 100644 index 00000000..18fe8260 --- /dev/null +++ b/coffin/template/._library.py diff --git a/coffin/template/._loader.py b/coffin/template/._loader.py Binary files differnew file mode 100644 index 00000000..3b699201 --- /dev/null +++ b/coffin/template/._loader.py diff --git a/coffin/template/._loaders.py b/coffin/template/._loaders.py Binary files differnew file mode 100644 index 00000000..e17a06d5 --- /dev/null +++ b/coffin/template/._loaders.py diff --git a/coffin/template/__init__.py b/coffin/template/__init__.py new file mode 100644 index 00000000..b487c8f5 --- /dev/null +++ b/coffin/template/__init__.py @@ -0,0 +1,93 @@ +from django.template import ( + Context as DjangoContext, + add_to_builtins as django_add_to_builtins, + get_library) +from jinja2 import Template as _Jinja2Template + +# Merge with ``django.template``. +from django.template import __all__ +from django.template import * + +# Override default library class with ours +from library import * + + +class Template(_Jinja2Template): + """Fixes the incompabilites between Jinja2's template class and + Django's. + + The end result should be a class that renders Jinja2 templates but + is compatible with the interface specfied by Django. + + 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): + # We accept the "origin" and "name" arguments, but discard them + # right away - Jinja's Template class (apparently) stores no + # equivalent information. + from coffin.common import env + + return env.from_string(template_string, template_class=cls) + + 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 + ``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: + context = {} + else: + context = dict_from_django_context(context) + assert isinstance(context, dict) # Required for **-operator. + return super(Template, self).render(**context) + + +def dict_from_django_context(context): + """Flattens a Django :class:`django.template.context.Context` object. + """ + if not isinstance(context, DjangoContext): + return context + else: + dict_ = {} + # Newest dicts are up front, so update from oldest to newest. + for subcontext in reversed(list(context)): + dict_.update(dict_from_django_context(subcontext)) + return dict_ + + +# libraries to load by default for a new environment +builtins = [] + + +def add_to_builtins(module_name): + """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. + + You can still use Django's own ``add_to_builtins`` to register + directly with Django and bypass Coffin. + + TODO: Allow passing path to (or reference of) extensions and + filters directly. This would make it easier to use this function + with 3rd party Jinja extensions that do not know about Coffin and + thus will not provide a Library object. + + 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(get_library(module_name)) + django_add_to_builtins(module_name) + + +add_to_builtins('coffin.template.defaulttags') +add_to_builtins('coffin.template.defaultfilters')
\ No newline at end of file diff --git a/coffin/template/defaultfilters.py b/coffin/template/defaultfilters.py new file mode 100644 index 00000000..c566a7d2 --- /dev/null +++ b/coffin/template/defaultfilters.py @@ -0,0 +1,99 @@ +"""Jinja2-ports of many of Django's default filters. + +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 + +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(jinja2_only=True) +def timesince(value, arg=None): + if value is None or isinstance(value, Undefined): + return u'' + from django.utils.timesince import timesince + if arg: + return timesince(value, arg) + return timesince(value) + +@register.filter(jinja2_only=True) +def timeuntil(value, arg=None): + if value is None or isinstance(value, Undefined): + return u'' + from django.utils.timesince import timeuntil + return timeuntil(date, arg) + +@register.filter(jinja2_only=True) +def date(value, arg=None): + if value is None or isinstance(value, Undefined): + return u'' + from django.conf import settings + from django.utils.dateformat import format + if arg is None: + arg = settings.DATE_FORMAT + return format(value, arg) + +@register.filter(jinja2_only=True) +def time(value, arg=None): + if value is None or isinstance(value, Undefined): + return u'' + from django.conf import settings + from django.utils.dateformat import time_format + if arg is None: + arg = settings.TIME_FORMAT + return time_format(value, arg) + +@register.filter(jinja2_only=True) +def truncatewords(value, length): + # Jinja2 has it's own ``truncate`` filter that supports word + # boundaries and more stuff, but cannot deal with HTML. + from django.utils.text import truncate_words + return truncate_words(value, int(length)) + +@register.filter(jinja2_only=True) +def truncatewords_html(value, length): + from django.utils.text import truncate_html_words + return truncate_html_words(value, int(length)) + +@register.filter(jinja2_only=True) +def pluralize(value, s1='s', s2=None): + """Like Django's pluralize-filter, but instead of using an optional + comma to separate singular and plural suffixes, it uses two distinct + parameters. + + It also is less forgiving if applied to values that do not allow + making a decision between singular and plural. + """ + if s2 is not None: + singular_suffix, plural_suffix = s1, s2 + else: + plural_suffix = s1 + singular_suffix = '' + + try: + if int(value) != 1: + return plural_suffix + except TypeError: # not a string or a number; maybe it's a list? + if len(value) != 1: + return plural_suffix + return singular_suffix + +@register.filter(jinja2_only=True) +def floatformat(value, arg=-1): + """Builds on top of Django's own version, but adds strict error + checking, staying with the philosophy. + """ + from django.template.defaultfilters import floatformat + from coffin.interop import django_filter_to_jinja2 + arg = int(arg) # raise exception + result = django_filter_to_jinja2(floatformat)(value, arg) + if result == '': # django couldn't handle the value + raise ValueError(value) + return result
\ No newline at end of file diff --git a/coffin/template/defaulttags.py b/coffin/template/defaulttags.py new file mode 100644 index 00000000..b9994257 --- /dev/null +++ b/coffin/template/defaulttags.py @@ -0,0 +1,364 @@ +from jinja2 import nodes +from jinja2.ext import Extension +from jinja2.exceptions import TemplateSyntaxError +from django.conf import settings +from coffin.template import Library + + +class LoadExtension(Extension): + """The load-tag is a no-op in Coffin. Instead, all template libraries + are always loaded. + + Note: Supporting a functioning load-tag in Jinja is tough, though + theoretically possible. The trouble is activating new extensions while + parsing is ongoing. The ``Parser.extensions`` dict of the current + parser instance needs to be modified, but apparently the only way to + get access would be by hacking the stack. + """ + + tags = set(['load']) + + def parse(self, parser): + while not parser.stream.current.type == 'block_end': + parser.stream.next() + return [] + + +"""class AutoescapeExtension(Extension): + ""#" + Template to output works in three phases in Jinja2: parsing, + generation (compilation, AST-traversal), and rendering (execution). + + Unfortunatly, the environment ``autoescape`` option comes into effect + during traversal, the part where we happen to have basically no control + over as an extension. It determines whether output is wrapped in + ``escape()`` calls. + + Solutions that could possibly work: + + * This extension could preprocess it's childnodes and wrap + everything output related inside the appropriate + ``Markup()`` or escape() call. + + * We could use the ``preprocess`` hook to insert the + appropriate ``|safe`` and ``|escape`` filters on a + string-basis. This is very unlikely to work well. + + There's also the issue of inheritance and just generally the nesting + of autoescape-tags to consider. + + Other things of note: + + * We can access ``parser.environment``, but that would only + affect the **parsing** of our child nodes. + + * In the commented-out code below we are trying to affect the + autoescape setting during rendering. As noted, this could be + necessary for rare border cases where custom extension use + the autoescape attribute. + + Both the above things would break Environment thread-safety though! + + Overall, it's not looking to good for this extension. + ""#" + + tags = ['autoescape'] + + def parse(self, parser): + lineno = parser.stream.next().lineno + + old_autoescape = parser.environment.autoescape + parser.environment.autoescape = True + try: + body = parser.parse_statements( + ['name:endautoescape'], drop_needle=True) + finally: + parser.environment.autoescape = old_autoescape + + # Not sure yet if the code below is necessary - it changes + # environment.autoescape during template rendering. If for example + # a CallBlock function accesses ``environment.autoescape``, it + # presumably is. + # This also should use try-finally though, which Jinja's API + # doesn't support either. We could fake that as well by using + # InternalNames that output the necessary indentation and keywords, + # but at this point it starts to get really messy. + # + # TODO: Actually, there's ``nodes.EnvironmentAttribute``. + #ae_setting = object.__new__(nodes.InternalName) + #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))) + #body.insert(len(body), nodes.Assign(ae_setting, temp)) + return body +""" + + +class URLExtension(Extension): + """Returns an absolute URL matching given view with its parameters. + + This is a way to define links that aren't tied to a particular URL + configuration:: + + {% url path.to.some_view arg1,arg2,name1=value1 %} + + Known differences to Django's url-Tag: + + - In Django, the view name may contain any non-space character. + Since Jinja's lexer does not identify whitespace to us, only + characters that make up valid identifers, plus dots and hyphens + are allowed. Note that identifers in Jinja 2 may not contain + non-ascii characters. + + As an alternative, you may specifify the view as a string, + which bypasses all these restrictions. It further allows you + to apply filters: + + {% url "меткаda.some-view"|afilter %} + """ + + tags = set(['url']) + + def parse(self, parser): + stream = parser.stream + + tag = stream.next() + + # get view name + if stream.current.test('string'): + viewname = parser.parse_primary() + else: + # parse valid tokens and manually build a string from them + bits = [] + name_allowed = True + while True: + if stream.current.test_any('dot', 'sub'): + bits.append(stream.next()) + name_allowed = True + elif stream.current.test('name') and name_allowed: + bits.append(stream.next()) + name_allowed = False + else: + break + viewname = nodes.Const("".join([b.value for b in bits])) + if not bits: + raise TemplateSyntaxError("'%s' requires path to view" % + tag.value, tag.lineno) + + # get arguments + args = [] + kwargs = [] + while not stream.current.test_any('block_end', 'name:as'): + if args or kwargs: + stream.expect('comma') + if stream.current.test('name') and stream.look().test('assign'): + key = nodes.Const(stream.next().value) + stream.skip() + value = parser.parse_expression() + kwargs.append(nodes.Pair(key, value, lineno=key.lineno)) + 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) + + # 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))) + return nodes.Assign(var, call_node) + # ...otherwise print it out. + else: + return nodes.Output([make_call_node()]).set_lineno(tag.lineno) + + @classmethod + def _reverse(self, viewname, args, kwargs, fail=True): + from django.core.urlresolvers import reverse, NoReverseMatch + + # Try to look up the URL twice: once given the view name, + # and again relative to what we guess is the "main" app. + url = '' + try: + url = reverse(viewname, args=args, kwargs=kwargs) + except NoReverseMatch: + projectname = settings.SETTINGS_MODULE.split('.')[0] + try: + url = reverse(projectname + '.' + viewname, + args=args, kwargs=kwargs) + except NoReverseMatch: + if fail: + raise + else: + return '' + + return url + + +class WithExtension(Extension): + """Adds a value to the context (inside this block) for caching and + easy access, just like the Django-version does. + + For example:: + + {% with person.some_sql_method as total %} + {{ total }} object{{ total|pluralize }} + {% endwith %} + + TODO: The new Scope node introduced in Jinja2 6334c1eade73 (the 2.2 + dev version) would help here, but we don't want to rely on that yet. + See also: + http://dev.pocoo.org/projects/jinja/browser/tests/test_ext.py + http://dev.pocoo.org/projects/jinja/ticket/331 + http://dev.pocoo.org/projects/jinja/ticket/329 + """ + + tags = set(['with']) + + def parse(self, parser): + lineno = parser.stream.next().lineno + + value = parser.parse_expression() + parser.stream.expect('name:as') + name = parser.stream.expect('name') + + body = parser.parse_statements(['name:endwith'], drop_needle=True) + return nodes.CallBlock( + self.call_method('_render_block', args=[value]), + [nodes.Name(name.value, 'store')], [], body).\ + set_lineno(lineno) + + def _render_block(self, value, caller=None): + return caller(value) + + +class CacheExtension(Extension): + """Exactly like Django's own tag, but supports full Jinja2 + expressiveness for all arguments. + + {% cache gettimeout()*2 "foo"+options.cachename %} + ... + {% endcache %} + + This actually means that there is a considerable incompatibility + to Django: In Django, the second argument is simply a name, but + interpreted as a literal string. This tag, with Jinja2 stronger + emphasis on consistent syntax, requires you to actually specify the + quotes around the name to make it a string. Otherwise, allowing + Jinja2 expressions would be very hard to impossible (one could use + a lookahead to see if the name is followed by an operator, and + evaluate it as an expression if so, or read it as a string if not. + TODO: This may not be the right choice. Supporting expressions + here is probably not very important, so compatibility should maybe + prevail. Unfortunately, it is actually pretty hard to be compatibly + in all cases, simply because Django's per-character parser will + just eat everything until the next whitespace and consider it part + of the fragment name, while we have to work token-based: ``x*2`` + would actually be considered ``"x*2"`` in Django, while Jinja2 + would give us three tokens: ``x``, ``*``, ``2``. + + General Syntax: + + {% cache [expire_time] [fragment_name] [var1] [var2] .. %} + .. some expensive processing .. + {% endcache %} + + Available by default (does not need to be loaded). + + Partly based on the ``FragmentCacheExtension`` from the Jinja2 docs. + + TODO: Should there be scoping issues with the internal dummy macro + limited access to certain outer variables in some cases, there is a + different way to write this. Generated code would look like this: + + internal_name = environment.extensions['..']._get_cache_value(): + if internal_name is not None: + yield internal_name + else: + internal_name = "" # or maybe use [] and append() for performance + internalname += "..." + internalname += "..." + internalname += "..." + environment.extensions['..']._set_cache_value(internalname): + yield internalname + + In other words, instead of using a CallBlock which uses a local + function and calls into python, we have to separate calls into + python, but put the if-else logic itself into the compiled template. + """ + + tags = set(['cache']) + + def parse(self, parser): + lineno = parser.stream.next().lineno + + expire_time = parser.parse_expression() + fragment_name = parser.parse_expression() + vary_on = [] + while not parser.stream.current.test('block_end'): + vary_on.append(parser.parse_expression()) + + body = parser.parse_statements(['name:endcache'], drop_needle=True) + + return nodes.CallBlock( + self.call_method('_cache_support', + [expire_time, fragment_name, + nodes.List(vary_on), nodes.Const(lineno)]), + [], [], body).set_lineno(lineno) + + def _cache_support(self, expire_time, fragm_name, vary_on, lineno, caller): + from django.core.cache import cache # delay depending in settings + from django.utils.http import urlquote + + 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) + + cache_key = u':'.join([fragm_name] + [urlquote(v) for v in vary_on]) + value = cache.get(cache_key) + if value is None: + value = caller() + cache.set(cache_key, value, expire_time) + return value + + +class SpacelessExtension(Extension): + """Removes whitespace between HTML tags, including tab and + newline characters. + + Works exactly like Django's own tag. + """ + + tags = ['spaceless'] + + def parse(self, parser): + lineno = parser.stream.next().lineno + body = parser.parse_statements(['name:endspaceless'], drop_needle=True) + return nodes.CallBlock( + self.call_method('_strip_spaces', [], [], None, None), + [], [], body + ).set_lineno(lineno) + + def _strip_spaces(self, caller=None): + from django.utils.html import strip_spaces_between_tags + return strip_spaces_between_tags(caller().strip()) + + +# nicer import names +load = LoadExtension +url = URLExtension +with_ = WithExtension +cache = CacheExtension +spaceless = SpacelessExtension + + +register = Library() +register.tag(load) +register.tag(url) +register.tag(with_) +register.tag(cache) +register.tag(spaceless)
\ No newline at end of file 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) diff --git a/coffin/template/loader.py b/coffin/template/loader.py new file mode 100644 index 00000000..1f2bbb1f --- /dev/null +++ b/coffin/template/loader.py @@ -0,0 +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))
diff --git a/coffin/template/loaders.py b/coffin/template/loaders.py new file mode 100644 index 00000000..cb42fd5d --- /dev/null +++ b/coffin/template/loaders.py @@ -0,0 +1,38 @@ +from jinja2 import loaders + + +def jinja_loader_from_django_loader(django_loader): + """Attempts to make a conversion from the given Django loader to an + similarly-behaving Jinja loader. + + :param django_loader: Django loader module string. + :return: The similarly-behaving Jinja loader, or None if a similar loader + could not be found. + """ + for substr, func in _JINJA_LOADER_BY_DJANGO_SUBSTR.iteritems(): + if substr in django_loader: + return func() + return None + + +def _make_jinja_app_loader(): + """Makes an 'app loader' for Jinja which acts like + :mod:`django.template.loaders.app_directories`. + """ + from django.template.loaders.app_directories import app_template_dirs + return loaders.FileSystemLoader(app_template_dirs) + + +def _make_jinja_filesystem_loader(): + """Makes a 'filesystem loader' for Jinja which acts like + :mod:`django.template.loaders.filesystem`. + """ + from django.conf import settings + return loaders.FileSystemLoader(settings.TEMPLATE_DIRS) + + +# Determine loaders from Django's conf. +_JINJA_LOADER_BY_DJANGO_SUBSTR = { # {substr: callable, ...} + 'app_directories': _make_jinja_app_loader, + 'filesystem': _make_jinja_filesystem_loader, +} diff --git a/coffin/views/__init__.py b/coffin/views/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/coffin/views/__init__.py diff --git a/coffin/views/defaults.py b/coffin/views/defaults.py new file mode 100644 index 00000000..4386a49b --- /dev/null +++ b/coffin/views/defaults.py @@ -0,0 +1,35 @@ +from django import http
+from django.template import Context, RequestContext
+from coffin.template.loader import render_to_string
+
+
+__all__ = ('page_not_found', 'server_error', 'shortcut')
+
+
+# no Jinja version for this needed
+from django.views.defaults import shortcut
+
+
+def page_not_found(request, template_name='404.html'):
+ """
+ Default 404 handler.
+
+ Templates: `404.html`
+ Context:
+ request_path
+ The path of the requested URL (e.g., '/app/pages/bad_page/')
+ """
+ content = render_to_string(template_name,
+ RequestContext(request, {'request_path': request.path}))
+ return http.HttpResponseNotFound(content)
+
+
+def server_error(request, template_name='500.html'):
+ """
+ 500 error handler.
+
+ Templates: `500.html`
+ Context: None
+ """
+ content = render_to_string(template_name, Context({}))
+ return http.HttpResponseServerError(content)
diff --git a/coffin/views/generic/._simple.py b/coffin/views/generic/._simple.py Binary files differnew file mode 100644 index 00000000..a1bb18de --- /dev/null +++ b/coffin/views/generic/._simple.py diff --git a/coffin/views/generic/__init__.py b/coffin/views/generic/__init__.py new file mode 100644 index 00000000..7ea80e4c --- /dev/null +++ b/coffin/views/generic/__init__.py @@ -0,0 +1 @@ +from django.views.generic import * diff --git a/coffin/views/generic/simple.py b/coffin/views/generic/simple.py new file mode 100644 index 00000000..ff7678b2 --- /dev/null +++ b/coffin/views/generic/simple.py @@ -0,0 +1,6 @@ +import inspect + +from django.views.generic.simple import * +from coffin.template import loader, RequestContext + +exec inspect.getsource(direct_to_template) |