From 1bfcc950ab6e5264bbaf2eba0ce3d3e23d671404 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 17 Feb 2015 22:49:59 +0100 Subject: [PATCH] Set context.template instead of context.engine while rendering. This opens more possibilities, like accessing context.template.origin. It also follows the chain of objects instead of following a shortcut. --- django/template/base.py | 24 +++++++++++------------ django/template/context.py | 30 +++++++++++++++++------------ django/template/defaulttags.py | 6 +++--- django/template/loader_tags.py | 6 +++--- django/templatetags/i18n.py | 2 +- docs/howto/custom-template-tags.txt | 22 +++++++++++---------- docs/ref/templates/upgrading.txt | 4 ++-- tests/template_tests/tests.py | 7 ++----- 8 files changed, 53 insertions(+), 48 deletions(-) diff --git a/django/template/base.py b/django/template/base.py index 16b76f51fd..325cd7e560 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -204,19 +204,19 @@ class Template(object): def render(self, context): "Display stage -- can be called many times" - # Set engine attribute here to avoid changing the signature of either - # Context.__init__ or Node.render. The engine is set only on the first - # call to render. Further calls e.g. for includes don't override it. - toplevel_render = context.engine is None + # Set context.template to the original template -- as opposed to + # extended or included templates -- during rendering. This may be + # used for accessing context.template.engine. + toplevel_render = context.template is None if toplevel_render: - context.engine = self.engine + context.template = self context.render_context.push() try: return self._render(context) finally: context.render_context.pop() if toplevel_render: - context.engine = None + context.template = None class Token(object): @@ -655,7 +655,7 @@ class FilterExpression(object): if ignore_failures: obj = None else: - string_if_invalid = context.engine.string_if_invalid + string_if_invalid = context.template.engine.string_if_invalid if string_if_invalid: if '%s' in string_if_invalid: return string_if_invalid % self.var @@ -847,7 +847,7 @@ class Variable(object): if getattr(current, 'do_not_call_in_templates', False): pass elif getattr(current, 'alters_data', False): - current = context.engine.string_if_invalid + current = context.template.engine.string_if_invalid else: try: # method call (assuming no args required) current = current() @@ -855,12 +855,12 @@ class Variable(object): try: getcallargs(current) except TypeError: # arguments *were* required - current = context.engine.string_if_invalid # invalid method call + current = context.template.engine.string_if_invalid # invalid method call else: raise except Exception as e: if getattr(e, 'silent_variable_failure', False): - current = context.engine.string_if_invalid + current = context.template.engine.string_if_invalid else: raise @@ -1257,9 +1257,9 @@ class Library(object): elif isinstance(getattr(file_name, 'template', None), Template): t = file_name.template elif not isinstance(file_name, six.string_types) and is_iterable(file_name): - t = context.engine.select_template(file_name) + t = context.template.engine.select_template(file_name) else: - t = context.engine.get_template(file_name) + t = context.template.engine.get_template(file_name) self.nodelist = t.nodelist new_context = context.new(_dict) # Copy across the CSRF token, if present, because diff --git a/django/template/context.py b/django/template/context.py index 01d21a159a..b4983d1909 100644 --- a/django/template/context.py +++ b/django/template/context.py @@ -123,7 +123,7 @@ class Context(BaseContext): "A stack container for variable context" def __init__(self, dict_=None, autoescape=True, current_app=_current_app_undefined, - use_l10n=None, use_tz=None, engine=None): + use_l10n=None, use_tz=None): if current_app is not _current_app_undefined: warnings.warn( "The current_app argument of Context is deprecated. Use " @@ -133,8 +133,10 @@ class Context(BaseContext): self._current_app = current_app self.use_l10n = use_l10n self.use_tz = use_tz - self.engine = engine self.render_context = RenderContext() + # Set to the original template during rendering -- as opposed to + # extended or included templates + self.template = None super(Context, self).__init__(dict_) @property @@ -192,11 +194,11 @@ class RequestContext(Context): """ def __init__(self, request, dict_=None, processors=None, current_app=_current_app_undefined, - use_l10n=None, use_tz=None, engine=None): + use_l10n=None, use_tz=None): # current_app isn't passed here to avoid triggering the deprecation # warning in Context.__init__. super(RequestContext, self).__init__( - dict_, use_l10n=use_l10n, use_tz=use_tz, engine=engine) + dict_, use_l10n=use_l10n, use_tz=use_tz) if current_app is not _current_app_undefined: warnings.warn( "The current_app argument of RequestContext is deprecated. " @@ -207,23 +209,27 @@ class RequestContext(Context): self._processors = () if processors is None else tuple(processors) self._processors_index = len(self.dicts) self.update({}) # placeholder for context processors output - self.engine = engine # re-run the setter in case engine is not None @property - def engine(self): - return self._engine + def template(self): + return self._template - @engine.setter - def engine(self, engine): - self._engine = engine + @template.setter + def template(self, template): + # Execute context processors when Template.render(self, context) sets + # context.template = self. Until then, since the context isn't tied to + # an engine, it has no way to know which context processors to apply. + self._template = template if hasattr(self, '_processors_index'): - if engine is None: + if template is None: # Unset context processors. self.dicts[self._processors_index] = {} else: # Set context processors for this engine. + processors = (template.engine.template_context_processors + + self._processors) updates = {} - for processor in engine.template_context_processors + self._processors: + for processor in processors: updates.update(processor(self.request)) self.dicts[self._processors_index] = updates diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 0dc6735ad6..fc8baf9611 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -211,7 +211,7 @@ class ForNode(Node): context[self.loopvars[0]] = item # In debug mode provide the source of the node which raised # the exception - if context.engine.debug: + if context.template.engine.debug: for node in self.nodelist_loop: try: nodelist.append(node.render(context)) @@ -392,7 +392,7 @@ class SsiNode(Node): def render(self, context): filepath = self.filepath.resolve(context) - if not include_is_allowed(filepath, context.engine.allowed_include_roots): + if not include_is_allowed(filepath, context.template.engine.allowed_include_roots): if settings.DEBUG: return "[Didn't have permission to include file]" else: @@ -404,7 +404,7 @@ class SsiNode(Node): output = '' if self.parsed: try: - t = Template(output, name=filepath, engine=context.engine) + t = Template(output, name=filepath, engine=context.template.engine) return t.render(context) except TemplateSyntaxError as e: if settings.DEBUG: diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py index e6635258a7..398914b6b7 100644 --- a/django/template/loader_tags.py +++ b/django/template/loader_tags.py @@ -107,7 +107,7 @@ class ExtendsNode(Node): if isinstance(getattr(parent, 'template', None), Template): # parent is a django.template.backends.django.Template return parent.template - return context.engine.get_template(parent) + return context.template.engine.get_template(parent) def render(self, context): compiled_parent = self.get_parent(context) @@ -148,7 +148,7 @@ class IncludeNode(Node): # Does this quack like a Template? if not callable(getattr(template, 'render', None)): # If not, we'll try get_template - template = context.engine.get_template(template) + template = context.template.engine.get_template(template) values = { name: var.resolve(context) for name, var in six.iteritems(self.extra_context) @@ -158,7 +158,7 @@ class IncludeNode(Node): with context.push(**values): return template.render(context) except Exception: - if context.engine.debug: + if context.template.engine.debug: raise return '' diff --git a/django/templatetags/i18n.py b/django/templatetags/i18n.py index 2a2b433508..bb81c56fb9 100644 --- a/django/templatetags/i18n.py +++ b/django/templatetags/i18n.py @@ -149,7 +149,7 @@ class BlockTranslateNode(Node): result = translation.pgettext(message_context, singular) else: result = translation.ugettext(singular) - default_value = context.engine.string_if_invalid + default_value = context.template.engine.string_if_invalid def render_value(key): if key in context: diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index 463c3bbfe5..bf385669c5 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -756,10 +756,11 @@ Notes: * The ``render()`` method is where the work actually happens. * ``render()`` should generally fail silently, particularly in a production - environment. In some cases however, particularly if ``context.engine.debug`` - is ``True``, this method may raise an exception to make debugging easier. - For example, several core tags raise ``django.template.TemplateSyntaxError`` - if they receive the wrong number or type of arguments. + environment. In some cases however, particularly if + ``context.template.engine.debug`` is ``True``, this method may raise an + exception to make debugging easier. For example, several core tags raise + ``django.template.TemplateSyntaxError`` if they receive the wrong number or + type of arguments. Ultimately, this decoupling of compilation and rendering results in an efficient template system, because a template can render multiple contexts @@ -795,16 +796,17 @@ This is not a very common situation, but it's useful if you're rendering a template yourself. For example:: def render(self, context): - t = context.engine.get_template('small_fragment.html') + t = context.template.engine.get_template('small_fragment.html') return t.render(Context({'var': obj}, autoescape=context.autoescape)) .. versionchanged:: 1.8 - The ``engine`` attribute of ``Context`` objects was added in Django 1.8. - :meth:`context.engine.get_template ` - must be used instead of :func:`django.template.loader.get_template` - because the latter now returns a wrapper whose ``render`` method doesn't - accept a :class:`~django.template.Context`. + The ``template`` attribute of ``Context`` objects was added in Django 1.8. + :meth:`context.template.engine.get_template + ` must be used instead of + :func:`django.template.loader.get_template` because the latter now returns + a wrapper whose ``render`` method doesn't accept a + :class:`~django.template.Context`. If we had neglected to pass in the current ``context.autoescape`` value to our new ``Context`` in this example, the results would have *always* been diff --git a/docs/ref/templates/upgrading.txt b/docs/ref/templates/upgrading.txt index b2ef23c4b8..6fdab53c40 100644 --- a/docs/ref/templates/upgrading.txt +++ b/docs/ref/templates/upgrading.txt @@ -162,7 +162,7 @@ instance in the ``render()`` method of a template tag, you can use the current You can write:: - template = context.engine.get_template('included.html') + template = context.template.engine.get_template('included.html') This will load the template with the current engine without triggering the multiple template engines machinery, which is usually the desired behavior. @@ -201,7 +201,7 @@ APIs. The multiple template engines machinery isn't involved here. Finally, if you have access to the current context, you can use the same trick as above:: - template = context.engine.from_string(template_code) + template = context.template.engine.from_string(template_code) ``Template()`` ============== diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py index cf37e1868b..77115d061a 100644 --- a/tests/template_tests/tests.py +++ b/tests/template_tests/tests.py @@ -516,9 +516,6 @@ class RequestContextTests(unittest.TestCase): self.assertEqual(len(ctx.dicts), 3) def test_context_comparable(self): - # Create an engine without any context processors. - engine = Engine() - test_data = {'x': 'y', 'v': 'z', 'd': {'o': object, 'a': 'b'}} # test comparing RequestContext to prevent problems if somebody @@ -526,8 +523,8 @@ class RequestContextTests(unittest.TestCase): request = RequestFactory().get('/') self.assertEqual( - RequestContext(request, dict_=test_data, engine=engine), - RequestContext(request, dict_=test_data, engine=engine)) + RequestContext(request, dict_=test_data), + RequestContext(request, dict_=test_data)) @ignore_warnings(category=RemovedInDjango20Warning)