From 64402d2fcc42c51eb85706a8ab03a0c7510efb20 Mon Sep 17 00:00:00 2001 From: James Socol Date: Mon, 21 Sep 2015 12:55:35 -0400 Subject: [PATCH] Use the Django>=1.7 app registry - Drops support for anything < 1.7. - Use `django.apps.apps.get_app_configs()` for template loaders and helper discovery. - Delays environment creation as long as possible, and caches it, should fix #50. - Removes direct access to jingo.env, use jingo.get_env() instead. - Removes env dependency from Registry(). - Moves Template() higher in the module and sets Environment().template_class directly. - Adds `django.contrib.admin.apps.SimpleAdminConfig` to test settings to ensure support for AppConfig classes, fixes #68. --- docs/index.rst | 4 +- fake_settings.py | 1 + jingo/__init__.py | 274 ++++++++++++++++++------------------- jingo/tests/test_basics.py | 23 ++-- jingo/tests/utils.py | 4 +- 5 files changed, 155 insertions(+), 151 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index a7cf88f..f7760aa 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -51,9 +51,9 @@ class with a literal string, e.g.:: then you'll need to change that code slightly, to:: - from jingo import env + from jingo import get_env - t = env.from_string('template_string') + t = get_env().from_string('template_string') and then the template will be rendered with all the same features that Jingo provides when rendering template files. diff --git a/fake_settings.py b/fake_settings.py index c425fd6..8ed2717 100644 --- a/fake_settings.py +++ b/fake_settings.py @@ -4,6 +4,7 @@ path = lambda *a: os.path.join(ROOT, *a) ROOT = os.path.dirname(os.path.abspath(__file__)) INSTALLED_APPS = ( + 'django.contrib.admin.apps.SimpleAdminConfig', 'jingo.tests.jinja_app', 'jingo.tests.django_app', ) diff --git a/jingo/__init__.py b/jingo/__init__.py index 8dce400..cc35179 100644 --- a/jingo/__init__.py +++ b/jingo/__init__.py @@ -7,6 +7,7 @@ import imp import logging import re +from django.apps import apps from django.conf import settings from django.template.base import Origin, TemplateDoesNotExist from django.template.loader import BaseLoader @@ -41,141 +42,6 @@ log = logging.getLogger('jingo') _helpers_loaded = False -class Environment(jinja2.Environment): - - def get_template(self, name, parent=None, globals=None): - """Make sure our helpers get loaded before any templates.""" - load_helpers() - return super(Environment, self).get_template(name, parent, globals) - - def from_string(self, source, globals=None, template_class=None): - load_helpers() - return super(Environment, self).from_string(source, globals, - template_class) - - -def get_env(): - """Configure and return a jinja2 Environment.""" - # Mimic Django's setup by loading templates from directories in - # TEMPLATE_DIRS and packages in INSTALLED_APPS. - x = ((jinja2.FileSystemLoader, settings.TEMPLATE_DIRS), - (jinja2.PackageLoader, settings.INSTALLED_APPS)) - loaders = [loader(p) for loader, places in x for p in places] - - opts = {'trim_blocks': True, - 'extensions': ['jinja2.ext.i18n'], - 'autoescape': True, - 'auto_reload': settings.DEBUG, - 'loader': jinja2.ChoiceLoader(loaders), - } - - if hasattr(settings, 'JINJA_CONFIG'): - if hasattr(settings.JINJA_CONFIG, '__call__'): - config = settings.JINJA_CONFIG() - else: - config = settings.JINJA_CONFIG - opts.update(config) - - e = Environment(**opts) - # Install null translations since gettext isn't always loaded up during - # testing. - if ('jinja2.ext.i18n' in e.extensions or - 'jinja2.ext.InternationalizationExtension' in e.extensions): - e.install_null_translations() - return e - - -def render_to_string(request, template, context=None): - """ - Render a template into a string. - """ - def get_context(): - c = {} if context is None else context.copy() - for processor in get_standard_processors(): - c.update(processor(request)) - return c - - # If it's not a Template, it must be a path to be loaded. - if not isinstance(template, jinja2.environment.Template): - template = env.get_template(template) - - return template.render(get_context()) - - -def load_helpers(): - """Try to import ``helpers.py`` from each app in INSTALLED_APPS.""" - # We want to wait as long as possible to load helpers so there aren't any - # weird circular imports with jingo. - global _helpers_loaded - if _helpers_loaded: - return - _helpers_loaded = True - - from jingo import helpers # noqa - - for app in settings.INSTALLED_APPS: - try: - app_path = import_module(app).__path__ - except AttributeError: - continue - - try: - imp.find_module('helpers', app_path) - except ImportError: - continue - - import_module('%s.helpers' % app) - - -class Register(object): - """Decorators to add filters and functions to the template Environment.""" - - def __init__(self, env): - self.env = env - - def filter(self, f=None, override=True): - """Adds the decorated function to Jinja's filter library.""" - def decorator(f): - @functools.wraps(f) - def wrapper(*args, **kw): - return f(*args, **kw) - return self.filter(wrapper, override) - - if not f: - return decorator - if override or f.__name__ not in self.env.filters: - self.env.filters[f.__name__] = f - return f - - def function(self, f=None, override=True): - """Adds the decorated function to Jinja's global namespace.""" - def decorator(f): - @functools.wraps(f) - def wrapper(*args, **kw): - return f(*args, **kw) - return self.function(wrapper, override) - - if not f: - return decorator - if override or f.__name__ not in self.env.globals: - self.env.globals[f.__name__] = f - return f - - def inclusion_tag(self, template): - """Adds a function to Jinja, but like Django's @inclusion_tag.""" - def decorator(f): - @functools.wraps(f) - def wrapper(*args, **kw): - context = f(*args, **kw) - t = env.get_template(template).render(context) - return jinja2.Markup(t) - return self.function(wrapper) - return decorator - -env = get_env() -register = Register(env) - - class Template(jinja2.Template): def render(self, context={}): @@ -206,9 +72,141 @@ class Template(jinja2.Template): return super(Template, self).render(context_dict) +class Environment(jinja2.Environment): + template_class = Template + + def get_template(self, name, parent=None, globals=None): + """Make sure our helpers get loaded before any templates.""" + load_helpers() + return super(Environment, self).get_template(name, parent, globals) + + def from_string(self, source, globals=None, template_class=None): + load_helpers() + return super(Environment, self).from_string(source, globals, + template_class) + + +_env = None + + +def get_env(): + """Configure and return a jinja2 Environment.""" + global _env + if _env: + return _env + # Mimic Django's setup by loading templates from directories in + # TEMPLATE_DIRS and packages in INSTALLED_APPS. + loaders = [jinja2.FileSystemLoader(d) for d in settings.TEMPLATE_DIRS] + loaders += [jinja2.PackageLoader(c.name) for c in apps.get_app_configs()] + + opts = { + 'trim_blocks': True, + 'extensions': ['jinja2.ext.i18n'], + 'autoescape': True, + 'auto_reload': settings.DEBUG, + 'loader': jinja2.ChoiceLoader(loaders), + } + + if hasattr(settings, 'JINJA_CONFIG'): + if hasattr(settings.JINJA_CONFIG, '__call__'): + config = settings.JINJA_CONFIG() + else: + config = settings.JINJA_CONFIG + opts.update(config) + + e = Environment(**opts) + # Install null translations since gettext isn't always loaded up during + # testing. + if ('jinja2.ext.i18n' in e.extensions or + 'jinja2.ext.InternationalizationExtension' in e.extensions): + e.install_null_translations() + _env = e + return e + + +def render_to_string(request, template, context=None): + """ + Render a template into a string. + """ + def get_context(): + c = {} if context is None else context.copy() + for processor in get_standard_processors(): + c.update(processor(request)) + return c + + # If it's not a Template, it must be a path to be loaded. + if not isinstance(template, jinja2.environment.Template): + template = get_env().get_template(template) + + return template.render(get_context()) + + +def load_helpers(): + """Try to import ``helpers.py`` from each app in INSTALLED_APPS.""" + # We want to wait as long as possible to load helpers so there aren't any + # weird circular imports with jingo. + global _helpers_loaded + if _helpers_loaded: + return + _helpers_loaded = True + + from jingo import helpers # noqa + + for config in apps.get_app_configs(): + try: + imp.find_module('helpers', config.name) + except ImportError: + continue + + import_module('%s.helpers' % config.name) + + +class Register(object): + """Decorators to add filters and functions to the template Environment.""" + def filter(self, f=None, override=True): + """Adds the decorated function to Jinja's filter library.""" + def decorator(f): + @functools.wraps(f) + def wrapper(*args, **kw): + return f(*args, **kw) + return self.filter(wrapper, override) + + if not f: + return decorator + if override or f.__name__ not in get_env().filters: + get_env().filters[f.__name__] = f + return f + + def function(self, f=None, override=True): + """Adds the decorated function to Jinja's global namespace.""" + def decorator(f): + @functools.wraps(f) + def wrapper(*args, **kw): + return f(*args, **kw) + return self.function(wrapper, override) + + if not f: + return decorator + if override or f.__name__ not in get_env().globals: + get_env().globals[f.__name__] = f + return f + + def inclusion_tag(self, template): + """Adds a function to Jinja, but like Django's @inclusion_tag.""" + def decorator(f): + @functools.wraps(f) + def wrapper(*args, **kw): + context = f(*args, **kw) + t = get_env().get_template(template).render(context) + return jinja2.Markup(t) + return self.function(wrapper) + return decorator + +register = Register() + + class Loader(BaseLoader): is_usable = True - env.template_class = Template def __init__(self): if has_engine: @@ -238,7 +236,7 @@ class Loader(BaseLoader): raise TemplateDoesNotExist(template_name) try: - template = env.get_template(template_name) + template = get_env().get_template(template_name) return template, template.filename except jinja2.TemplateNotFound: raise TemplateDoesNotExist(template_name) @@ -248,7 +246,7 @@ class Loader(BaseLoader): raise TemplateDoesNotExist(template_name) try: - template = env.get_template(template_name) + template = get_env().get_template(template_name) except jinja2.TemplateNotFound: raise TemplateDoesNotExist(template_name) diff --git a/jingo/tests/test_basics.py b/jingo/tests/test_basics.py index 2d1e113..29256a8 100644 --- a/jingo/tests/test_basics.py +++ b/jingo/tests/test_basics.py @@ -12,10 +12,12 @@ except ImportError: import jingo -@patch('jingo.env') -def test_render(mock_env): +@patch('jingo.get_env') +def test_render(mock_get_env): mock_template = Mock() + mock_env = Mock() mock_env.get_template.return_value = mock_template + mock_get_env.return_value = mock_env response = render(Mock(), sentinel.template, status=32) mock_env.get_template.assert_called_with(sentinel.template) @@ -24,19 +26,22 @@ def test_render(mock_env): eq_(response.status_code, 32) -@patch('jingo.env') -def test_render_to_string(mock_env): +@patch('jingo.get_env') +def test_render_to_string(mock_get_env): template = jinja2.environment.Template('The answer is {{ answer }}') rendered = jingo.render_to_string(Mock(), template, {'answer': 42}) eq_(rendered, 'The answer is 42') -@patch('jingo.env.get_template') -def test_inclusion_tag(get_template): +def test_inclusion_tag(): @jingo.register.inclusion_tag('xx.html') def tag(x): return {'z': x} - get_template.return_value = jinja2.environment.Template('<{{ z }}>') - t = jingo.env.from_string('{{ tag(1) }}') - eq_('<1>', t.render()) + + env = jingo.get_env() + with patch.object(env, 'get_template') as mock_get_template: + temp = jinja2.environment.Template('<{{ z }}>') + mock_get_template.return_value = temp + t = env.from_string('{{ tag(1) }}') + eq_('<1>', t.render()) diff --git a/jingo/tests/utils.py b/jingo/tests/utils.py index 6a52057..2a3c9dd 100644 --- a/jingo/tests/utils.py +++ b/jingo/tests/utils.py @@ -1,7 +1,7 @@ from django.test.html import HTMLParseError, parse_html from nose.tools import eq_ -from jingo import env +from jingo import get_env def htmleq_(html1, html2, msg=None): @@ -31,5 +31,5 @@ def assert_and_parse_html(html, user_msg, msg): def render(s, context={}): - t = env.from_string(s) + t = get_env().from_string(s) return t.render(context)