From a391b17ad24bc5f255a3928c23c158c79004c656 Mon Sep 17 00:00:00 2001 From: Matthew Somerville Date: Thu, 4 Jun 2015 12:50:07 +0100 Subject: [PATCH] Fixed #23516 -- Added caching of include tag Template objects This also speeds up for loops that render the same template multiple times. --- django/template/loader_tags.py | 16 ++++++++++++++-- docs/releases/1.9.txt | 3 +++ .../syntax_tests/test_if_changed.py | 14 ++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py index de77ecbb7a9..8d8afde5d92 100644 --- a/django/template/loader_tags.py +++ b/django/template/loader_tags.py @@ -171,6 +171,8 @@ class ExtendsNode(Node): class IncludeNode(Node): + context_key = '__include_context' + def __init__(self, template, *args, **kwargs): self.template = template self.extra_context = kwargs.pop('extra_context', {}) @@ -178,12 +180,22 @@ class IncludeNode(Node): super(IncludeNode, self).__init__(*args, **kwargs) def render(self, context): + """ + Render the specified template and context. Cache the template object + in render_context to avoid reparsing and loading when used in a for + loop. + """ try: template = self.template.resolve(context) # Does this quack like a Template? if not callable(getattr(template, 'render', None)): - # If not, we'll try get_template - template = context.template.engine.get_template(template) + # If not, we'll try our cache, and get_template() + template_name = template + cache = context.render_context.setdefault(self.context_key, {}) + template = cache.get(template_name) + if template is None: + template = context.template.engine.get_template(template_name) + cache[template_name] = template values = { name: var.resolve(context) for name, var in six.iteritems(self.extra_context) diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index c6467d9f652..6b42418ee41 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -314,6 +314,9 @@ Templates * The ``timesince`` and ``timeuntil`` filters were improved to deal with leap years when given large time spans. +* The ``include`` tag now caches parsed templates objects during template + rendering, speeding up reuse in places such as for loops. + Requests and Responses ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/template_tests/syntax_tests/test_if_changed.py b/tests/template_tests/syntax_tests/test_if_changed.py index 09cdc34a882..ee67c642181 100644 --- a/tests/template_tests/syntax_tests/test_if_changed.py +++ b/tests/template_tests/syntax_tests/test_if_changed.py @@ -196,3 +196,17 @@ class IfChangedTests(SimpleTestCase): template = self.engine.from_string('{% ifchanged %}{% cycle "1st time" "2nd time" %}{% endifchanged %}') output = template.render(Context({})) self.assertEqual(output, '1st time') + + def test_include(self): + """ + #23516 -- This works as a regression test only if the cached loader + isn't used. Hence we don't use the @setup decorator. + """ + engine = Engine(loaders=[ + ('django.template.loaders.locmem.Loader', { + 'template': '{% for x in vars %}{% include "include" %}{% endfor %}', + 'include': '{% ifchanged %}{{ x }}{% endifchanged %}', + }), + ]) + output = engine.render_to_string('template', dict(vars=[1, 1, 2, 2, 3, 3])) + self.assertEqual(output, "123")