From bf1fc6afad9a36d633049203dd583c6277e95252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Elsd=C3=B6rfer?= Date: Wed, 8 Feb 2012 13:54:19 +0100 Subject: [PATCH] Support Django cached loader. Closes #23. --- coffin/common.py | 25 +++++++++++++------ coffin/template/loaders.py | 50 ++++++++++++++++++++++++++++++++++++-- tests/test_env.py | 45 +++++++++++++++++++++++++--------- 3 files changed, 99 insertions(+), 21 deletions(-) diff --git a/coffin/common.py b/coffin/common.py index 6dd7ed2..c8b273e 100644 --- a/coffin/common.py +++ b/coffin/common.py @@ -48,18 +48,29 @@ def _get_loaders(self): 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) + 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 diff --git a/coffin/template/loaders.py b/coffin/template/loaders.py index 1206bda..1c81d85 100644 --- a/coffin/template/loaders.py +++ b/coffin/template/loaders.py @@ -1,7 +1,7 @@ from jinja2 import loaders -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. @@ -13,7 +13,7 @@ def jinja_loader_from_django_loader(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 @@ -33,8 +33,54 @@ 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, } + + +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] diff --git a/tests/test_env.py b/tests/test_env.py index 3466cbf..744a89b 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -11,15 +11,36 @@ def test_i18n(): assert get_env().from_string('{{ _("test") }}').render() == 'test' -def test_django_loader_replace(): - 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) +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' +