Merge branch 'master' of git://github.com/coffin/coffin

This commit is contained in:
Davide Setti 2013-01-07 12:21:20 +01:00
commit 6c159eb609
22 changed files with 558 additions and 39 deletions

10
CHANGES
View File

@ -1,3 +1,11 @@
0.3.7 (2012-09-29)
- Support for Django 1.3 class-based views (Kirill Zaitsev,
Dmitry Panteleev)
- Loader for *.jinja files (Aarni Koskela)
- makemessages command now supports whitespace stripping tags
(Dmitry Panteleev)
- Now supports Django's CachedLoader.
0.3.6 (2011-09-09)
- Re-release of 0.3.5, containing the correct repository
- Re-release of 0.3.5, containing the correct repository
state this time.

View File

@ -14,7 +14,7 @@ reasonable.
__all__ = ('__version__', '__build__', '__docformat__', 'get_revision')
__version__ = (0, 3, '7', 'dev')
__version__ = (0, 3, '8', 'dev')
__docformat__ = 'restructuredtext en'
import os

View File

@ -28,7 +28,7 @@ class CoffinEnvironment(Environment):
# the proper priority), so we want to assign to these attributes.
self.filters = all_ext['filters'].copy()
self.filters.update(filters)
self.globals = all_ext['globals'].copy()
self.globals.update(all_ext['globals'])
self.globals.update(globals)
self.tests = all_ext['tests'].copy()
self.tests.update(tests)
@ -48,20 +48,30 @@ class CoffinEnvironment(Environment):
loaders = []
from coffin.template.loaders import jinja_loader_from_django_loader
from jinja2.loaders import BaseLoader as JinjaLoader
from django.conf import settings
_loaders = getattr(settings, 'JINJA2_TEMPLATE_LOADERS', settings.TEMPLATE_LOADERS)
for loader in _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.
if isinstance(loader, JinjaLoader):
loaders.append(loader)
return loaders
else:
loader_name = args = None
if isinstance(loader, basestring):
loader_name = loader
args = []
elif isinstance(loader, (tuple, list)):
loader_name = loader[0]
args = loader[1]
if loader_name:
loader_obj = jinja_loader_from_django_loader(loader_name, args)
if loader_obj:
loaders.append(loader_obj)
continue
warnings.warn('Cannot translate loader: %s' % loader)
return loaders
def _get_templatelibs(self):
"""Return an iterable of template ``Library`` instances.
@ -70,30 +80,35 @@ class CoffinEnvironment(Environment):
register all libraries globally.
"""
from django.conf import settings
from django.template import get_library, InvalidTemplateLibrary
from django.template import (
get_library, import_library, InvalidTemplateLibrary)
libs = []
for a in settings.INSTALLED_APPS:
for app in settings.INSTALLED_APPS:
ns = app + '.templatetags'
try:
path = __import__(a + '.templatetags', {}, {}, ['__file__']).__file__
path = __import__(ns, {}, {}, ['__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' or f.startswith('.'):
for filename in os.listdir(path):
if filename == '__init__.py' or filename.startswith('.'):
continue
if f.endswith('.py'):
if filename.endswith('.py'):
try:
# TODO: will need updating when #6587 lands
# libs.append(get_library(
# "django.templatetags.%s" % os.path.splitext(f)[0]))
l = get_library(os.path.splitext(f)[0])
module = "%s.%s" % (ns, os.path.splitext(filename)[0])
l = import_library(module)
libs.append(l)
except InvalidTemplateLibrary:
pass
# In addition to loading application libraries, support a custom list
for libname in getattr(settings, 'JINJA2_DJANGO_TEMPLATETAG_LIBRARIES', ()):
libs.append(get_library(libname))
return libs
def _get_all_extensions(self):
@ -139,25 +154,28 @@ class CoffinEnvironment(Environment):
# Next, add the globally defined extensions
extensions.extend(list(getattr(settings, 'JINJA2_EXTENSIONS', [])))
def from_setting(setting):
def from_setting(setting, values_must_be_callable = False):
retval = {}
setting = getattr(settings, setting, {})
if isinstance(setting, dict):
for key, value in setting.iteritems():
retval[key] = callable(value) and value or get_callable(value)
if values_must_be_callable and not callable(value):
value = get_callable(value)
retval[key] = value
else:
for value in setting:
value = callable(value) and value or get_callable(value)
if values_must_be_callable and not callable(value):
value = get_callable(value)
retval[value.__name__] = value
return retval
filters.update(from_setting('JINJA2_FILTERS'))
tests.update(from_setting('JINJA2_TESTS', True))
filters.update(from_setting('JINJA2_FILTERS', True))
globals.update(from_setting('JINJA2_GLOBALS'))
tests.update(from_setting('JINJA2_TESTS'))
# Finally, add extensions defined in application's templatetag libraries
for lib in self._get_templatelibs():
_load_lib(lib)
attrs.update(getattr(lib, 'jinja2_environment_attrs', {}))
return dict(
extensions=extensions,

47
coffin/contrib/loader.py Normal file
View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
"""
A Django template loader wrapper for Coffin that intercepts
requests for "*.jinja" templates, rendering them with Coffin
instead of Django templates.
Usage:
TEMPLATE_LOADERS = (
'coffin.contrib.loader.AppLoader',
'coffin.contrib.loader.FileSystemLoader',
)
"""
from os.path import splitext
from coffin.common import env
from django.conf import settings
from django.template.loaders import app_directories, filesystem
JINJA2_DEFAULT_TEMPLATE_EXTENSION = getattr(settings,
'JINJA2_DEFAULT_TEMPLATE_EXTENSION', ('.jinja',))
if isinstance(JINJA2_DEFAULT_TEMPLATE_EXTENSION, basestring):
JINJA2_DEFAULT_TEMPLATE_EXTENSION = (JINJA2_DEFAULT_TEMPLATE_EXTENSION,)
class LoaderMixin(object):
is_usable = True
def load_template(self, template_name, template_dirs=None):
extension = splitext(template_name)[1]
if not extension in JINJA2_DEFAULT_TEMPLATE_EXTENSION:
return super(LoaderMixin, self).load_template(template_name,
template_dirs)
template = env.get_template(template_name)
return template, template.filename
class FileSystemLoader(LoaderMixin, filesystem.Loader):
pass
class AppLoader(LoaderMixin, app_directories.Loader):
pass

View File

View File

@ -0,0 +1,38 @@
from coffin import template
from django.contrib.staticfiles.storage import staticfiles_storage
from coffin.templatetags.static import StaticExtension
register = template.Library()
class StaticExtension(StaticExtension):
"""Implements the {% static %} tag as provided by the ``staticfiles``
contrib module.
Rreturns the URL to a file using staticfiles' storage backend.
Usage::
{% static path [as varname] %}
Examples::
{% static "myapp/css/base.css" %}
{% static variable_with_path %}
{% static "myapp/css/base.css" as admin_base_css %}
{% static variable_with_path as varname %}
"""
@classmethod
def get_statc_url(cls, path):
return super(StaticExtension, cls).get_statc_url(
staticfiles_storage.url(path))
register.tag(StaticExtension)
def static(path):
return StaticExtension.get_static_url(path)

View File

@ -27,24 +27,40 @@ http://stackoverflow.com/questions/2090717/getting-translation-strings-for-jinja
import re
from django.core.management.commands import makemessages
from django.utils.translation import trans_real
from django.template import BLOCK_TAG_START, BLOCK_TAG_END
strip_whitespace_right = re.compile(r"(%s-?\s*(trans|pluralize).*?-%s)\s+" % (BLOCK_TAG_START, BLOCK_TAG_END), re.U)
strip_whitespace_left = re.compile(r"\s+(%s-\s*(endtrans|pluralize).*?-?%s)" % (BLOCK_TAG_START, BLOCK_TAG_END), re.U)
def strip_whitespaces(src):
src = strip_whitespace_left.sub(r'\1', src)
src = strip_whitespace_right.sub(r'\1', src)
return src
class Command(makemessages.Command):
def handle(self, *args, **options):
old_endblock_re = trans_real.endblock_re
old_block_re = trans_real.block_re
old_templatize = trans_real.templatize
# Extend the regular expressions that are used to detect
# translation blocks with an "OR jinja-syntax" clause.
trans_real.endblock_re = re.compile(
trans_real.endblock_re.pattern + '|' + r"""^\s*endtrans$""")
trans_real.endblock_re.pattern + '|' + r"""^-?\s*endtrans\s*-?$""")
trans_real.block_re = re.compile(
trans_real.block_re.pattern + '|' + r"""^\s*trans(?:\s+(?!'|")(?=.*?=.*?)|$)""")
trans_real.block_re.pattern + '|' + r"""^-?\s*trans(?:\s+(?!'|")(?=.*?=.*?)|-?$)""")
trans_real.plural_re = re.compile(
trans_real.plural_re.pattern + '|' + r"""^\s*pluralize(?:\s+.+|$)""")
trans_real.plural_re.pattern + '|' + r"""^-?\s*pluralize(?:\s+.+|-?$)""")
def my_templatize(src, origin=None):
new_src = strip_whitespaces(src)
return old_templatize(new_src, origin)
trans_real.templatize = my_templatize
try:
super(Command, self).handle(*args, **options)
finally:
trans_real.endblock_re = old_endblock_re
trans_real.block_re = old_block_re
trans_real.templatize = old_templatize

View File

@ -5,7 +5,7 @@ from django.http import HttpResponse
from django.shortcuts import *
__all__ = ('render_to_string', 'render_to_response',)
__all__ = ('render_to_string', 'render_to_response', 'render')
# Is within ``template.loader`` as per Django specification -
@ -23,3 +23,29 @@ def render_to_response(template_name, dictionary=None, context_instance=None,
"""
rendered = render_to_string(template_name, dictionary, context_instance)
return HttpResponse(rendered, mimetype=mimetype)
def render(request, *args, **kwargs):
"""
Returns a HttpResponse whose content is filled with the result of calling
coffin.template.loader.render_to_string() with the passed arguments.
Uses a RequestContext by default.
"""
httpresponse_kwargs = {
'content_type': kwargs.pop('content_type', None),
'status': kwargs.pop('status', None),
}
if 'context_instance' in kwargs:
context_instance = kwargs.pop('context_instance')
if kwargs.get('current_app', None):
raise ValueError('If you provide a context_instance you must '
'set its current_app before calling render()')
else:
current_app = kwargs.pop('current_app', None)
context_instance = RequestContext(request, current_app=current_app)
kwargs['context_instance'] = context_instance
return HttpResponse(render_to_string(*args, **kwargs),
**httpresponse_kwargs)

View File

@ -88,6 +88,10 @@ def add_to_builtins(module_name):
You can still use Django's own ``add_to_builtins`` to register
directly with Django and bypass Coffin.
Once thing that is special about Coffin is that because {% load %}
is not supported in Coffin, *everything* it provides must be
registered through the builtins.
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
@ -103,4 +107,5 @@ def add_to_builtins(module_name):
add_to_builtins('coffin.template.defaulttags')
add_to_builtins('coffin.template.defaultfilters')
add_to_builtins('coffin.templatetags.static')

View File

@ -1,7 +1,10 @@
import re
from jinja2 import loaders
match_loader = re.compile(r'^(django|coffin)\.')
def jinja_loader_from_django_loader(django_loader):
def jinja_loader_from_django_loader(django_loader, args=None):
"""Attempts to make a conversion from the given Django loader to an
similarly-behaving Jinja loader.
@ -9,9 +12,11 @@ def jinja_loader_from_django_loader(django_loader):
:return: The similarly-behaving Jinja loader, or None if a similar loader
could not be found.
"""
if not match_loader.match(django_loader):
return None
for substr, func in _JINJA_LOADER_BY_DJANGO_SUBSTR.iteritems():
if substr in django_loader:
return func()
return func(*(args or []))
return None
@ -31,8 +36,56 @@ def _make_jinja_filesystem_loader():
return loaders.FileSystemLoader(settings.TEMPLATE_DIRS)
def _make_jinja_cached_loader(*loaders):
"""Makes a loader for Jinja which acts like
:mod:`django.template.loaders.cached`.
"""
return JinjaCachedLoader(
[jinja_loader_from_django_loader(l) for l in loaders])
# Determine loaders from Django's conf.
_JINJA_LOADER_BY_DJANGO_SUBSTR = { # {substr: callable, ...}
'app_directories': _make_jinja_app_loader,
'filesystem': _make_jinja_filesystem_loader,
'cached': _make_jinja_cached_loader,
'AppLoader': _make_jinja_app_loader,
'FileSystemLoader': _make_jinja_filesystem_loader,
}
class JinjaCachedLoader(loaders.BaseLoader):
"""A "sort of" port of of Django's "cached" template loader
to Jinja 2. It exists primarily to support Django's full
TEMPLATE_LOADERS syntax.
However, note that it does not behave exactly like Django's cached
loader: Rather than caching the compiled template, it only caches
the template source, and recompiles the template every time. This is
due to the way the Jinja2/Coffin loader setup works: The ChoiceLoader,
which Coffin uses at the root to select from any of the configured
loaders, calls the ``get_source`` method of each loader directly,
bypassing ``load``. Our loader can therefore only hook into the process
BEFORE template compilation.
Caching the compiled templates by implementing ``load`` would only
work if this loader instance were the root loader. See also the comments
in Jinja2's BaseLoader class.
Note that Jinja2 has an environment-wide bytecode cache (i.e. it caches
compiled templates), that can function alongside with this class.
Note further that Jinja2 has an environment-wide template cache (via the
``auto_reload`` environment option), which duplicate the functionality
of this class entirely, and should be preferred when possible.
"""
def __init__(self, subloaders):
self.loader = loaders.ChoiceLoader(subloaders)
self.template_cache = {}
def get_source(self, environment, template):
key = (environment, template)
if key not in self.template_cache:
result = self.loader.get_source(environment, template)
self.template_cache[key] = result
return self.template_cache[key]

View File

View File

@ -0,0 +1,130 @@
try:
from urllib.parse import urljoin
except ImportError: # Python 2
from urlparse import urljoin
from coffin.template import Library
from jinja2.ext import Extension
from jinja2 import nodes
from django.utils.encoding import iri_to_uri
register = Library()
class PrefixExtension(Extension):
def parse(self, parser):
stream = parser.stream
lineno = stream.next().lineno
call_node = self.call_method('render')
if stream.next_if('name:as'):
var = nodes.Name(stream.expect('name').value, 'store')
return nodes.Assign(var, call_node).set_lineno(lineno)
else:
return nodes.Output([call_node]).set_lineno(lineno)
def render(self, name):
raise NotImplementedError()
@classmethod
def get_uri_setting(cls, name):
try:
from django.conf import settings
except ImportError:
prefix = ''
else:
prefix = iri_to_uri(getattr(settings, name, ''))
return prefix
class GetStaticPrefixExtension(PrefixExtension):
"""
Populates a template variable with the static prefix,
``settings.STATIC_URL``.
Usage::
{% get_static_prefix [as varname] %}
Examples::
{% get_static_prefix %}
{% get_static_prefix as static_prefix %}
"""
tags = set(['get_static_prefix'])
def render(self):
return self.get_uri_setting('STATIC_URL')
class GetMediaPrefixExtension(PrefixExtension):
"""
Populates a template variable with the media prefix,
``settings.MEDIA_URL``.
Usage::
{% get_media_prefix [as varname] %}
Examples::
{% get_media_prefix %}
{% get_media_prefix as media_prefix %}
"""
tags = set(['get_media_prefix'])
def render(self):
return self.get_uri_setting('STATIC_URL')
class StaticExtension(PrefixExtension):
"""
Joins the given path with the STATIC_URL setting.
Usage::
{% static path [as varname] %}
Examples::
{% static "myapp/css/base.css" %}
{% static variable_with_path %}
{% static "myapp/css/base.css" as admin_base_css %}
{% static variable_with_path as varname %}
"""
tags = set(['static'])
def parse(self, parser):
stream = parser.stream
lineno = stream.next().lineno
path = parser.parse_expression()
call_node = self.call_method('get_statc_url', args=[path])
if stream.next_if('name:as'):
var = nodes.Name(stream.expect('name').value, 'store')
return nodes.Assign(var, call_node).set_lineno(lineno)
else:
return nodes.Output([call_node]).set_lineno(lineno)
@classmethod
def get_statc_url(cls, path):
return urljoin(PrefixExtension.get_uri_setting("STATIC_URL"), path)
register.tag(GetStaticPrefixExtension)
register.tag(GetMediaPrefixExtension)
register.tag(StaticExtension)
def static(path):
return StaticExtension.get_static_url(path)

View File

@ -0,0 +1,9 @@
from coffin.template.response import TemplateResponse
def template_response(cls):
"""
A decorator to enforce class_based generic views
to use coffin TemplateResponse
"""
cls.response_class = TemplateResponse
return cls

View File

@ -0,0 +1,13 @@
import django.views.generic.base as _generic_base
from coffin.template.response import TemplateResponse as JinjaTemplateResponse
class TemplateResponseMixin(_generic_base.TemplateResponseMixin):
"""
A mixin that can be used to render a template using Jinja.
"""
response_class = JinjaTemplateResponse
class TemplateView(TemplateResponseMixin, _generic_base.TemplateView):
"""
A view that renders a template using Jinja.
"""

View File

@ -0,0 +1,50 @@
from coffin.views.generic.detail import SingleObjectTemplateResponseMixin
from coffin.views.generic.list import MultipleObjectTemplateResponseMixin
import django.views.generic.dates as _generic_dates
class ArchiveIndexView(MultipleObjectTemplateResponseMixin, _generic_dates.BaseArchiveIndexView):
"""
Equivalent of django generic view ArchiveIndexView, but uses Jinja template renderer.
"""
template_name_suffix = '_archive'
class YearArchiveView(MultipleObjectTemplateResponseMixin, _generic_dates.BaseYearArchiveView):
"""
Equivalent of django generic view YearArchiveView, but uses Jinja template renderer.
"""
template_name_suffix = '_archive_year'
class MonthArchiveView(MultipleObjectTemplateResponseMixin, _generic_dates.BaseMonthArchiveView):
"""
Equivalent of django generic view MonthArchiveView, but uses Jinja template renderer.
"""
template_name_suffix = '_archive_month'
class WeekArchiveView(MultipleObjectTemplateResponseMixin, _generic_dates.BaseWeekArchiveView):
"""
Equivalent of django generic view WeekArchiveView, but uses Jinja template renderer.
"""
template_name_suffix = '_archive_week'
class DayArchiveView(MultipleObjectTemplateResponseMixin, _generic_dates.BaseDayArchiveView):
"""
Equivalent of django generic view DayArchiveView, but uses Jinja template renderer.
"""
template_name_suffix = "_archive_day"
class TodayArchiveView(MultipleObjectTemplateResponseMixin, _generic_dates.BaseTodayArchiveView):
"""
Equivalent of django generic view TodayArchiveView, but uses Jinja template renderer.
"""
template_name_suffix = "_archive_day"
class DateDetailView(SingleObjectTemplateResponseMixin, _generic_dates.BaseDateDetailView):
"""
Equivalent of django generic view DateDetailView, but uses Jinja template renderer.
"""
template_name_suffix = '_detail'

View File

@ -0,0 +1,12 @@
import django.views.generic.detail as _generic_detail
from coffin.views.generic.base import TemplateResponseMixin as JinjaTemplateResponseMixin
class SingleObjectTemplateResponseMixin(JinjaTemplateResponseMixin, _generic_detail.SingleObjectTemplateResponseMixin):
"""
Equivalent of django mixin SingleObjectTemplateResponseMixin, but uses Jinja template renderer.
"""
class DetailView(SingleObjectTemplateResponseMixin, _generic_detail.BaseDetailView):
"""
Equivalent of django generic view DetailView, but uses Jinja template renderer.
"""

View File

@ -0,0 +1,30 @@
from coffin.views.generic.base import TemplateResponseMixin
from coffin.views.generic.detail import SingleObjectTemplateResponseMixin
import django.views.generic.edit as _generic_edit
class FormView(TemplateResponseMixin, _generic_edit.BaseFormView):
"""
Equivalent of django generic view FormView, but uses Jinja template renderer.
"""
class CreateView(SingleObjectTemplateResponseMixin, _generic_edit.BaseCreateView):
"""
Equivalent of django generic view CreateView, but uses Jinja template renderer.
"""
template_name_suffix = '_form'
class UpdateView(SingleObjectTemplateResponseMixin, _generic_edit.BaseUpdateView):
"""
Equivalent of django generic view UpdateView, but uses Jinja template renderer.
"""
template_name_suffix = '_form'
class DeleteView(SingleObjectTemplateResponseMixin, _generic_edit.BaseDeleteView):
"""
Equivalent of django generic view DeleteView, but uses Jinja template renderer.
"""
template_name_suffix = '_confirm_delete'

View File

@ -0,0 +1,12 @@
import django.views.generic.list as _generic_list
from coffin.views.generic.base import TemplateResponseMixin as JinjaTemplateResponseMixin
class MultipleObjectTemplateResponseMixin(JinjaTemplateResponseMixin, _generic_list.MultipleObjectTemplateResponseMixin):
"""
Equivalent of django mixin MultipleObjectTemplateResponseMixin, but uses Jinja template renderer.
"""
class ListView(MultipleObjectTemplateResponseMixin, _generic_list.BaseListView):
"""
Equivalent of django generic view ListView, but uses Jinja template renderer.
"""

View File

@ -1,9 +1,10 @@
from os import path
import sys
def setup_package():
# setup Django with our test demo project
sys.path.insert(0, path.join(path.dirname(__file__), 'res', 'apps'))
from django.core.management import setup_environ
import settings
setup_environ(settings)
# Setup Django with our test demo project. We need to do this in global
# module code rather than setup_package(), because we want it to run
# before any module-wide imports in any of the test modules.
sys.path.insert(0, path.join(path.dirname(__file__), 'res', 'apps'))
from django.core.management import setup_environ
import settings
setup_environ(settings)

46
tests/test_env.py Normal file
View File

@ -0,0 +1,46 @@
"""Test construction of the implicitly provided JinjaEnvironment,
in the common.py module.
"""
from coffin.common import get_env
from django.test.utils import override_settings
def test_i18n():
with override_settings(USE_I18N=True):
assert get_env().from_string('{{ _("test") }}').render() == 'test'
class TestLoaders:
def test_django_loader_replace(self):
from coffin.template.loaders import jinja_loader_from_django_loader
from jinja2 import loaders
# Test replacement of filesystem loader
l = jinja_loader_from_django_loader('django.template.loaders.filesystem.Loader')
assert isinstance(l, loaders.FileSystemLoader)
# Since we don't do exact matches for the loader string, make sure we
# are not replacing loaders that are outside the Django namespace.
l = jinja_loader_from_django_loader('djangoaddon.template.loaders.filesystem.Loader')
assert not isinstance(l, loaders.FileSystemLoader)
def test_cached_loader(self):
from jinja2 import loaders
with override_settings(TEMPLATE_LOADERS=[
('django.template.loaders.cached.Loader', (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)),]):
env = get_env()
assert len(env.loader.loaders) == 1
cached_loader = get_env().loader.loaders[0]
assert hasattr(cached_loader, 'template_cache')
assert len(cached_loader.loader.loaders) == 2
assert isinstance(cached_loader.loader.loaders[0], loaders.FileSystemLoader)
# the cached loader can find a template too.
assert env.loader.load(env, 'render-x.html').render({'x': 'foo'}) == 'foo'

5
tests/test_shortcuts.py Normal file
View File

@ -0,0 +1,5 @@
def test_render():
"""Test the render shortcut."""
from coffin.shortcuts import render
response = render(None, 'render-x.html', {'x': 'foo'})
assert response.content == 'foo'