Code

Ticket #6262: cache_templates.2.diff

File cache_templates.2.diff, 28.6 KB (added by mmalone, 5 years ago)

template caching, refactored loaders, fixed template tags

Line 
1Index: django/template/__init__.py
2===================================================================
3--- django/template/__init__.py (revision 11374)
4+++ django/template/__init__.py (working copy)
5@@ -175,7 +175,11 @@
6 
7     def render(self, context):
8         "Display stage -- can be called many times"
9-        return self.nodelist.render(context)
10+        context.parser_context.push()
11+        try:
12+            return self.nodelist.render(context)
13+        finally:
14+            context.parser_context.pop()
15 
16 def compile_string(template_string, origin):
17     "Compiles template_string into NodeList ready for rendering"
18Index: django/template/loaders/app_directories.py
19===================================================================
20--- django/template/loaders/app_directories.py  (revision 11374)
21+++ django/template/loaders/app_directories.py  (working copy)
22@@ -9,6 +9,7 @@
23 from django.conf import settings
24 from django.core.exceptions import ImproperlyConfigured
25 from django.template import TemplateDoesNotExist
26+from django.template.loader import BaseLoader
27 from django.utils._os import safe_join
28 from django.utils.importlib import import_module
29 
30@@ -27,29 +28,38 @@
31 # It won't change, so convert it to a tuple to save memory.
32 app_template_dirs = tuple(app_template_dirs)
33 
34-def get_template_sources(template_name, template_dirs=None):
35-    """
36-    Returns the absolute paths to "template_name", when appended to each
37-    directory in "template_dirs". Any paths that don't lie inside one of the
38-    template dirs are excluded from the result set, for security reasons.
39-    """
40-    if not template_dirs:
41-        template_dirs = app_template_dirs
42-    for template_dir in template_dirs:
43-        try:
44-            yield safe_join(template_dir, template_name)
45-        except UnicodeDecodeError:
46-            # The template dir name was a bytestring that wasn't valid UTF-8.
47-            raise
48-        except ValueError:
49-            # The joined path was located outside of template_dir.
50-            pass
51+class Loader(BaseLoader):
52+    is_usable = True
53 
54+    def get_template_sources(self, template_name, template_dirs=None):
55+        """
56+        Returns the absolute paths to "template_name", when appended to each
57+        directory in "template_dirs". Any paths that don't lie inside one of the
58+        template dirs are excluded from the result set, for security reasons.
59+        """
60+        if not template_dirs:
61+            template_dirs = app_template_dirs
62+        for template_dir in template_dirs:
63+            try:
64+                yield safe_join(template_dir, template_name)
65+            except UnicodeDecodeError:
66+                # The template dir name was a bytestring that wasn't valid UTF-8.
67+                raise
68+            except ValueError:
69+                # The joined path was located outside of template_dir.
70+                pass
71+
72+    def load_template_source(self, template_name, template_dirs=None):
73+        for filepath in self.get_template_sources(template_name, template_dirs):
74+            try:
75+                return (open(filepath).read().decode(settings.FILE_CHARSET), filepath)
76+            except IOError:
77+                pass
78+        raise TemplateDoesNotExist, template_name
79+
80+loader = Loader()
81+
82 def load_template_source(template_name, template_dirs=None):
83-    for filepath in get_template_sources(template_name, template_dirs):
84-        try:
85-            return (open(filepath).read().decode(settings.FILE_CHARSET), filepath)
86-        except IOError:
87-            pass
88-    raise TemplateDoesNotExist, template_name
89+    # For backwards compatibility
90+    return loader.load_template_source(template_name, template_dirs)
91 load_template_source.is_usable = True
92Index: django/template/loaders/filesystem.py
93===================================================================
94--- django/template/loaders/filesystem.py       (revision 11374)
95+++ django/template/loaders/filesystem.py       (working copy)
96@@ -4,38 +4,49 @@
97 
98 from django.conf import settings
99 from django.template import TemplateDoesNotExist
100+from django.template.loader import BaseLoader
101 from django.utils._os import safe_join
102 
103-def get_template_sources(template_name, template_dirs=None):
104-    """
105-    Returns the absolute paths to "template_name", when appended to each
106-    directory in "template_dirs". Any paths that don't lie inside one of the
107-    template dirs are excluded from the result set, for security reasons.
108-    """
109-    if not template_dirs:
110-        template_dirs = settings.TEMPLATE_DIRS
111-    for template_dir in template_dirs:
112-        try:
113-            yield safe_join(template_dir, template_name)
114-        except UnicodeDecodeError:
115-            # The template dir name was a bytestring that wasn't valid UTF-8.
116-            raise
117-        except ValueError:
118-            # The joined path was located outside of this particular
119-            # template_dir (it might be inside another one, so this isn't
120-            # fatal).
121-            pass
122+class Loader(BaseLoader):
123+    is_usable = True
124 
125+    def get_template_sources(self, template_name, template_dirs=None):
126+        """
127+        Returns the absolute paths to "template_name", when appended to each
128+        directory in "template_dirs". Any paths that don't lie inside one of the
129+        template dirs are excluded from the result set, for security reasons.
130+        """
131+        if not template_dirs:
132+            template_dirs = settings.TEMPLATE_DIRS
133+        for template_dir in template_dirs:
134+            try:
135+                yield safe_join(template_dir, template_name)
136+            except UnicodeDecodeError:
137+                # The template dir name was a bytestring that wasn't valid UTF-8.
138+                raise
139+            except ValueError:
140+                # The joined path was located outside of this particular
141+                # template_dir (it might be inside another one, so this isn't
142+                # fatal).
143+                pass
144+
145+    def load_template_source(self, template_name, template_dirs=None):
146+        tried = []
147+        for filepath in self.get_template_sources(template_name, template_dirs):
148+            try:
149+                return (open(filepath).read().decode(settings.FILE_CHARSET), filepath)
150+            except IOError:
151+                tried.append(filepath)
152+        if tried:
153+            error_msg = "Tried %s" % tried
154+        else:
155+            error_msg = "Your TEMPLATE_DIRS setting is empty. Change it to point to at least one template directory."
156+        raise TemplateDoesNotExist, error_msg
157+    load_template_source.is_usable = True
158+
159+loader = Loader()
160+
161 def load_template_source(template_name, template_dirs=None):
162-    tried = []
163-    for filepath in get_template_sources(template_name, template_dirs):
164-        try:
165-            return (open(filepath).read().decode(settings.FILE_CHARSET), filepath)
166-        except IOError:
167-            tried.append(filepath)
168-    if tried:
169-        error_msg = "Tried %s" % tried
170-    else:
171-        error_msg = "Your TEMPLATE_DIRS setting is empty. Change it to point to at least one template directory."
172-    raise TemplateDoesNotExist, error_msg
173+    # For backwards compatibility
174+    return loader.load_template_source(template_name, template_dirs)
175 load_template_source.is_usable = True
176Index: django/template/loaders/eggs.py
177===================================================================
178--- django/template/loaders/eggs.py     (revision 11374)
179+++ django/template/loaders/eggs.py     (working copy)
180@@ -6,20 +6,29 @@
181     resource_string = None
182 
183 from django.template import TemplateDoesNotExist
184+from django.template.loader import BaseLoader
185 from django.conf import settings
186 
187+class Loader(BaseLoader):
188+    is_usable = resource_string is not None
189+
190+    def load_template_source(self, template_name, template_dirs=None):
191+        """
192+        Loads templates from Python eggs via pkg_resource.resource_string.
193+
194+        For every installed app, it tries to get the resource (app, template_name).
195+        """
196+        if resource_string is not None:
197+            pkg_name = 'templates/' + template_name
198+            for app in settings.INSTALLED_APPS:
199+                try:
200+                    return (resource_string(app, pkg_name).decode(settings.FILE_CHARSET), 'egg:%s:%s' % (app, pkg_name))
201+                except:
202+                    pass
203+        raise TemplateDoesNotExist, template_name
204+
205+loader = Loader()
206+
207 def load_template_source(template_name, template_dirs=None):
208-    """
209-    Loads templates from Python eggs via pkg_resource.resource_string.
210-
211-    For every installed app, it tries to get the resource (app, template_name).
212-    """
213-    if resource_string is not None:
214-        pkg_name = 'templates/' + template_name
215-        for app in settings.INSTALLED_APPS:
216-            try:
217-                return (resource_string(app, pkg_name).decode(settings.FILE_CHARSET), 'egg:%s:%s' % (app, pkg_name))
218-            except:
219-                pass
220-    raise TemplateDoesNotExist, template_name
221+    return loader.load_template_source(template_name, template_dirs)
222 load_template_source.is_usable = resource_string is not None
223Index: django/template/loaders/cached.py
224===================================================================
225--- django/template/loaders/cached.py   (revision 0)
226+++ django/template/loaders/cached.py   (revision 0)
227@@ -0,0 +1,47 @@
228+"""
229+Wrapper class that takes a list of template loaders as an argument and attempts
230+to load templates from them in order, caching the result.
231+"""
232+
233+from django.template import TemplateDoesNotExist
234+from django.template.loader import BaseLoader, get_template_from_string, find_template_loader, make_origin
235+from django.utils.importlib import import_module
236+from django.core.exceptions import ImproperlyConfigured
237+
238+class Loader(BaseLoader):
239+    is_usable = True
240+    template_cache = {}
241+
242+    def __init__(self, loaders):
243+        self._loaders = loaders
244+        self._cached_loaders = []
245+
246+    #@property
247+    def loaders(self):
248+        # Resolve loaders on demand to avoid circular imports
249+        if not self._cached_loaders:
250+            for loader in self._loaders:
251+                self._cached_loaders.append(find_template_loader(loader))
252+        return self._cached_loaders
253+    loaders = property(loaders)
254+
255+    def find_template(self, name, dirs=None):
256+        for loader in self.loaders:
257+            try:
258+                template, display_name = loader(name, dirs)
259+                return (template, make_origin(display_name, loader, name, dirs))
260+            except TemplateDoesNotExist:
261+                pass
262+        raise TemplateDoesNotExist, name
263+
264+    def load_template(self, template_name, template_dirs=None):
265+        if template_name not in self.template_cache:
266+            template, origin = self.find_template(template_name, template_dirs)
267+            if not hasattr(template, 'render'):
268+                template = get_template_from_string(template, origin, template_name)
269+            self.template_cache[template_name] = (template, origin)
270+        return self.template_cache[template_name]
271+
272+    def reset(self):
273+        "Empty the template cache."
274+        self.template_cache = {}
275Index: django/template/defaulttags.py
276===================================================================
277--- django/template/defaulttags.py      (revision 11374)
278+++ django/template/defaulttags.py      (working copy)
279@@ -39,11 +39,14 @@
280 
281 class CycleNode(Node):
282     def __init__(self, cyclevars, variable_name=None):
283-        self.cycle_iter = itertools_cycle(cyclevars)
284+        self.cyclevars = cyclevars
285         self.variable_name = variable_name
286 
287     def render(self, context):
288-        value = self.cycle_iter.next().resolve(context)
289+        if self not in context.parser_context:
290+            context.parser_context[self] = {'cycle_iter': itertools_cycle(self.cyclevars)}
291+        cycle_iter = context.parser_context[self]['cycle_iter']
292+        value = cycle_iter.next().resolve(context)
293         if self.variable_name:
294             context[self.variable_name] = value
295         return value
296Index: django/template/context.py
297===================================================================
298--- django/template/context.py  (revision 11374)
299+++ django/template/context.py  (working copy)
300@@ -14,6 +14,7 @@
301         self.dicts = [dict_]
302         self.autoescape = autoescape
303         self.current_app = current_app
304+        self.parser_context = ParserContext()
305 
306     def __repr__(self):
307         return repr(self.dicts)
308@@ -68,6 +69,52 @@
309         self.dicts = [other_dict] + self.dicts
310         return other_dict
311 
312+class ParserContext(object):
313+    """A stack container for storing Template state."""
314+    def __init__(self, dict_=None):
315+        dict_ = dict_ or {}
316+        self.dicts = [dict_]
317+
318+    def __repr__(self):
319+        return repr(self.dicts)
320+   
321+    def __iter__(self):
322+        for d in self.dicts[-1]:
323+            yield d
324+
325+    def push(self):
326+        d = {}
327+        self.dicts.append(d)
328+        return d
329+
330+    def pop(self):
331+        if len(self.dicts) == 1:
332+            raise ContextPopException
333+        return self.dicts.pop()
334+
335+    def __setitem__(self, key, value):
336+        "Set a variable in the current context"
337+        self.dicts[-1][key] = value
338+
339+    def __getitem__(self, key):
340+        "Get a variable's value from the current context"
341+        return self.dicts[-1][key]
342+   
343+    def __delitem__(self, key):
344+        "Deletes a variable from the current context"
345+        del self.dicts[-1][key]
346+
347+    def has_key(self, key):
348+        return key in self.dicts[-1]
349+
350+    __contains__ = has_key
351+
352+    def get(self, key, otherwise=None):
353+        d = self.dicts[-1]
354+        if key in d:
355+            return d[key]
356+        return otherwise
357+
358 # This is a function rather than module-level procedural code because we only
359 # want it to execute if somebody uses RequestContext.
360 def get_standard_processors():
361Index: django/template/loader_tags.py
362===================================================================
363--- django/template/loader_tags.py      (revision 11374)
364+++ django/template/loader_tags.py      (working copy)
365@@ -1,14 +1,43 @@
366 from django.template import TemplateSyntaxError, TemplateDoesNotExist, Variable
367 from django.template import Library, Node, TextNode
368-from django.template.loader import get_template, get_template_from_string, find_template_source
369+from django.template.loader import get_template
370 from django.conf import settings
371 from django.utils.safestring import mark_safe
372 
373 register = Library()
374 
375+BLOCK_CONTEXT_KEY = 'block_context'
376+
377 class ExtendsError(Exception):
378     pass
379 
380+class BlockContext(object):
381+    def __init__(self):
382+        # Dictionary of FIFO queues.
383+        self.blocks = {}
384+
385+    def add_blocks(self, blocks):
386+        for name, block in blocks.iteritems():
387+            if name in self.blocks:
388+                self.blocks[name].insert(0, block)
389+            else:
390+                self.blocks[name] = [block]
391+
392+    def pop(self, name):
393+        try:
394+            return self.blocks[name].pop()
395+        except (IndexError, KeyError):
396+            return None
397+
398+    def push(self, name, block):
399+        self.blocks[name].append(block)
400+
401+    def get_block(self, name):
402+        try:
403+            return self.blocks[name][-1]
404+        except (IndexError, KeyError):
405+            return None
406+
407 class BlockNode(Node):
408     def __init__(self, name, nodelist, parent=None):
409         self.name, self.nodelist, self.parent = name, nodelist, parent
410@@ -17,25 +46,32 @@
411         return "<Block Node: %s. Contents: %r>" % (self.name, self.nodelist)
412 
413     def render(self, context):
414+        block_context = context.parser_context.get(BLOCK_CONTEXT_KEY, None)
415         context.push()
416-        # Save context in case of block.super().
417-        self.context = context
418-        context['block'] = self
419-        result = self.nodelist.render(context)
420+        if block_context is None:
421+            context['block'] = self
422+            result = self.nodelist.render(context)
423+        else:
424+            push = block = block_context.pop(self.name)
425+            if block is None:
426+                block = self
427+            # Create new block so we can store context without thread-safety issues.
428+            block = BlockNode(block.name, block.nodelist)
429+            block.context = context
430+            context['block'] = block
431+            result = block.nodelist.render(context)
432+            if push is not None:
433+                block_context.push(self.name, push)
434         context.pop()
435         return result
436 
437     def super(self):
438-        if self.parent:
439-            return mark_safe(self.parent.render(self.context))
440+        parser_context = self.context.parser_context
441+        if (BLOCK_CONTEXT_KEY in parser_context and
442+            parser_context[BLOCK_CONTEXT_KEY].get_block(self.name) is not None):
443+            return mark_safe(self.render(self.context))
444         return ''
445 
446-    def add_parent(self, nodelist):
447-        if self.parent:
448-            self.parent.add_parent(nodelist)
449-        else:
450-            self.parent = BlockNode(self.name, nodelist)
451-
452 class ExtendsNode(Node):
453     must_be_first = True
454 
455@@ -43,7 +79,8 @@
456         self.nodelist = nodelist
457         self.parent_name, self.parent_name_expr = parent_name, parent_name_expr
458         self.template_dirs = template_dirs
459-
460+        self.blocks = dict([(n.name, n) for n in nodelist.get_nodes_by_type(BlockNode)])
461+
462     def __repr__(self):
463         if self.parent_name_expr:
464             return "<ExtendsNode: extends %s>" % self.parent_name_expr.token
465@@ -61,41 +98,35 @@
466         if hasattr(parent, 'render'):
467             return parent # parent is a Template object
468         try:
469-            source, origin = find_template_source(parent, self.template_dirs)
470+            return get_template(parent)
471         except TemplateDoesNotExist:
472             raise TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent
473-        else:
474-            return get_template_from_string(source, origin, parent)
475 
476     def render(self, context):
477         compiled_parent = self.get_parent(context)
478-        parent_blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)])
479-        for block_node in self.nodelist.get_nodes_by_type(BlockNode):
480-            # Check for a BlockNode with this node's name, and replace it if found.
481-            try:
482-                parent_block = parent_blocks[block_node.name]
483-            except KeyError:
484-                # This BlockNode wasn't found in the parent template, but the
485-                # parent block might be defined in the parent's *parent*, so we
486-                # add this BlockNode to the parent's ExtendsNode nodelist, so
487-                # it'll be checked when the parent node's render() is called.
488 
489-                # Find out if the parent template has a parent itself
490-                for node in compiled_parent.nodelist:
491-                    if not isinstance(node, TextNode):
492-                        # If the first non-text node is an extends, handle it.
493-                        if isinstance(node, ExtendsNode):
494-                            node.nodelist.append(block_node)
495-                        # Extends must be the first non-text node, so once you find
496-                        # the first non-text node you can stop looking.
497-                        break
498-            else:
499-                # Keep any existing parents and add a new one. Used by BlockNode.
500-                parent_block.parent = block_node.parent
501-                parent_block.add_parent(parent_block.nodelist)
502-                parent_block.nodelist = block_node.nodelist
503-        return compiled_parent.render(context)
504+        if BLOCK_CONTEXT_KEY not in context.parser_context:
505+            context.parser_context[BLOCK_CONTEXT_KEY] = BlockContext()
506+        block_context = context.parser_context[BLOCK_CONTEXT_KEY]
507 
508+        # Add the block nodes from this node to the block context
509+        block_context.add_blocks(self.blocks)
510+
511+        # If this block's parent doesn't have an extends node it is the root,
512+        # and its block nodes also need to be added to the block context.
513+        for node in compiled_parent.nodelist:
514+            # The ExtendsNode has to be the first non-text node.
515+            if not isinstance(node, TextNode):
516+                if not isinstance(node, ExtendsNode):
517+                    blocks = dict([(n.name, n) for n in
518+                                   compiled_parent.nodelist.get_nodes_by_type(BlockNode)])
519+                    block_context.add_blocks(blocks)
520+                break
521+
522+        # Call render on nodelist explicitly so the block context stays
523+        # the same.
524+        return compiled_parent.nodelist.render(context)
525+
526 class ConstantIncludeNode(Node):
527     def __init__(self, template_path):
528         try:
529Index: django/template/loader.py
530===================================================================
531--- django/template/loader.py   (revision 11374)
532+++ django/template/loader.py   (working copy)
533@@ -27,6 +27,20 @@
534 
535 template_source_loaders = None
536 
537+class BaseLoader(object):
538+    is_usable = False
539+
540+    def __call__(self, template_name, template_dirs=None):
541+        return self.load_template(template_name, template_dirs)
542+
543+    def load_template(self, template_name, template_dirs=None):
544+        source, origin = self.load_template_source(template_name, template_dirs)
545+        template = get_template_from_string(source, name=template_name)
546+        return template, origin
547+   
548+    def load_template_from_source(self, template_name, template_dirs=None):
549+        raise NotImplementedError
550+
551 class LoaderOrigin(Origin):
552     def __init__(self, display_name, loader, name, dirs):
553         super(LoaderOrigin, self).__init__(display_name)
554@@ -41,29 +55,37 @@
555     else:
556         return None
557 
558-def find_template_source(name, dirs=None):
559+def find_template_loader(loader):
560+    if callable(loader):
561+        return loader
562+    elif isinstance(loader, basestring):
563+        i = loader.rfind('.')
564+        module, attr = loader[:i], loader[i+1:]
565+        try:
566+            mod = import_module(module)
567+        except ImportError, e:
568+            raise ImproperlyConfigured, 'Error importing template source loader %s: "%s"' % (module, e)
569+        try:
570+            func = getattr(mod, attr)
571+        except AttributeError:
572+            raise ImproperlyConfigured, 'Module "%s" does not define a "%s" callable template source loader' % (module, attr)
573+        if not func.is_usable:
574+            import warnings
575+            warnings.warn("Your TEMPLATE_LOADERS setting includes %r, but your Python installation doesn't support that type of template loading. Consider removing that line from TEMPLATE_LOADERS." % loader)
576+        else:
577+            return func
578+    else:
579+        raise ImproperlyConfigured, 'Loader does not define a "load_template" callable template source loader'
580+
581+def find_template(name, dirs=None):
582     # Calculate template_source_loaders the first time the function is executed
583     # because putting this logic in the module-level namespace may cause
584     # circular import errors. See Django ticket #1292.
585     global template_source_loaders
586     if template_source_loaders is None:
587         loaders = []
588-        for path in settings.TEMPLATE_LOADERS:
589-            i = path.rfind('.')
590-            module, attr = path[:i], path[i+1:]
591-            try:
592-                mod = import_module(module)
593-            except ImportError, e:
594-                raise ImproperlyConfigured, 'Error importing template source loader %s: "%s"' % (module, e)
595-            try:
596-                func = getattr(mod, attr)
597-            except AttributeError:
598-                raise ImproperlyConfigured, 'Module "%s" does not define a "%s" callable template source loader' % (module, attr)
599-            if not func.is_usable:
600-                import warnings
601-                warnings.warn("Your TEMPLATE_LOADERS setting includes %r, but your Python installation doesn't support that type of template loading. Consider removing that line from TEMPLATE_LOADERS." % path)
602-            else:
603-                loaders.append(func)
604+        for loader in settings.TEMPLATE_LOADERS:
605+            loaders.append(find_template_loader(loader))
606         template_source_loaders = tuple(loaders)
607     for loader in template_source_loaders:
608         try:
609@@ -78,8 +100,10 @@
610     Returns a compiled Template object for the given template name,
611     handling template inheritance recursively.
612     """
613-    source, origin = find_template_source(template_name)
614-    template = get_template_from_string(source, origin, template_name)
615+    template, origin = find_template(template_name)
616+    if not hasattr(template, 'render'):
617+        # template needs to be compiled
618+        template = get_template_from_string(template, origin, template_name)
619     return template
620 
621 def get_template_from_string(source, origin=None, name=None):
622Index: tests/regressiontests/templates/tests.py
623===================================================================
624--- tests/regressiontests/templates/tests.py    (revision 11374)
625+++ tests/regressiontests/templates/tests.py    (working copy)
626@@ -105,8 +105,8 @@
627                 # Fix expected sources so they are normcased and abspathed
628                 expected_sources = [os.path.normcase(os.path.abspath(s)) for s in expected_sources]
629             # Test the two loaders (app_directores and filesystem).
630-            func1 = lambda p, t: list(app_directories.get_template_sources(p, t))
631-            func2 = lambda p, t: list(filesystem.get_template_sources(p, t))
632+            func1 = lambda p, t: list(app_directories.loader.get_template_sources(p, t))
633+            func2 = lambda p, t: list(filesystem.loader.get_template_sources(p, t))
634             for func in (func1, func2):
635                 if isinstance(expected_sources, list):
636                     self.assertEqual(func(path, template_dirs), expected_sources)
637@@ -197,8 +197,11 @@
638             except KeyError:
639                 raise template.TemplateDoesNotExist, template_name
640 
641+        from django.template.loaders.cached import Loader
642+        cache_loader = Loader((test_template_loader,))
643+
644         old_template_loaders = loader.template_source_loaders
645-        loader.template_source_loaders = [test_template_loader]
646+        loader.template_source_loaders = [cache_loader]
647 
648         failures = []
649         tests = template_tests.items()
650@@ -231,20 +234,22 @@
651             for invalid_str, result in [('', normal_string_result),
652                                         (expected_invalid_str, invalid_string_result)]:
653                 settings.TEMPLATE_STRING_IF_INVALID = invalid_str
654-                try:
655-                    test_template = loader.get_template(name)
656-                    output = self.render(test_template, vals)
657-                except ContextStackException:
658-                    failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Context stack was left imbalanced" % (invalid_str, name))
659-                    continue
660-                except Exception:
661-                    exc_type, exc_value, exc_tb = sys.exc_info()
662-                    if exc_type != result:
663-                        tb = '\n'.join(traceback.format_exception(exc_type, exc_value, exc_tb))
664-                        failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Got %s, exception: %s\n%s" % (invalid_str, name, exc_type, exc_value, tb))
665-                    continue
666-                if output != result:
667-                    failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Expected %r, got %r" % (invalid_str, name, result, output))
668+                for cached in (False, True):
669+                    try:
670+                        test_template = loader.get_template(name)
671+                        output = self.render(test_template, vals)
672+                    except ContextStackException:
673+                        failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Context stack was left imbalanced" % (cached, invalid_str, name))
674+                        continue
675+                    except Exception:
676+                        exc_type, exc_value, exc_tb = sys.exc_info()
677+                        if exc_type != result:
678+                            tb = '\n'.join(traceback.format_exception(exc_type, exc_value, exc_tb))
679+                            failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Got %s, exception: %s\n%s" % (cached, invalid_str, name, exc_type, exc_value, tb))
680+                        continue
681+                    if output != result:
682+                        failures.append("Template test (Cached='%s', TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Expected %r, got %r" % (cached, invalid_str, name, result, output))
683+                cache_loader.reset()
684 
685             if 'LANGUAGE_CODE' in vals[1]:
686                 deactivate()