imports and includes "with context" are passed the full context now, not only the...
authorArmin Ronacher <armin.ronacher@active-4.com>
Sat, 4 Oct 2008 16:06:57 +0000 (18:06 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Sat, 4 Oct 2008 16:06:57 +0000 (18:06 +0200)
--HG--
branch : trunk

CHANGES
docs/templates.rst
jinja2/compiler.py
jinja2/environment.py
tests/test_imports.py

diff --git a/CHANGES b/CHANGES
index 3a05bca08fcffb1d1e49cb52e11bc479b5503d75..7a5d0c2ed0a77f6d9e5916bd76013a4b69c778c7 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -34,6 +34,9 @@ Version 2.1
 
 - the template context is now weakref-able
 
+- inclusions and imports "with context" forward all variables now, not only
+  the initial context.
+
 Version 2.0
 -----------
 (codename jinjavitus, released on July 17th 2008)
index 0b686865db60f04cc4736b13fd51f2395c3de39d..0c01eee7372318ac6d63e9645abdb320e76e6a4e 100644 (file)
@@ -794,6 +794,19 @@ Here two examples::
     {% from 'forms.html' import input with context %}
     {% include 'header.html' without context %}
 
+.. admonition:: Note
+
+    In Jinja 2.0 the context that was passed to the included template
+    did not include variables define in the template.  As a matter of
+    fact this did not work::
+
+        {% for box in boxes %}
+            {% include "render_box.html" %}
+        {% endfor %}
+
+    The included template ``render_box.html`` is not able to access
+    `box` in Jinja 2.0, but in Jinja 2.1.
+
 
 .. _expressions:
 
index 0686c84a7e73e455511fd3e88652fd9053cf7775..e6b2de0c429d54cb144146900552519d059caba7 100644 (file)
@@ -520,23 +520,38 @@ class CodeGenerator(NodeVisitor):
                 self.writeline('%s = environment.%s[%r]' %
                                (mapping[name], dependency, name))
 
-    def collect_shadowed(self, frame, extra_vars=()):
+    def push_scope(self, frame, extra_vars=()):
         """This function returns all the shadowed variables in a dict
         in the form name: alias and will write the required assignments
         into the current scope.  No indentation takes place.
 
+        This also predefines locally declared variables from the loop
+        body because under some circumstances it may be the case that
+
         `extra_vars` is passed to `Identifiers.find_shadowed`.
         """
         aliases = {}
         for name in frame.identifiers.find_shadowed(extra_vars):
             aliases[name] = ident = self.temporary_identifier()
             self.writeline('%s = l_%s' % (ident, name))
+        to_declare = set()
+        for name in frame.identifiers.declared_locally:
+            if name not in aliases:
+                to_declare.add('l_' + name)
+        if to_declare:
+            self.writeline(' = '.join(to_declare) + ' = missing')
         return aliases
 
-    def restore_shadowed(self, aliases):
-        """Restore all aliases."""
+    def pop_scope(self, aliases, frame):
+        """Restore all aliases and delete unused variables."""
         for name, alias in aliases.iteritems():
             self.writeline('l_%s = %s' % (name, alias))
+        to_delete = set()
+        for name in frame.identifiers.declared_locally:
+            if name not in aliases:
+                to_delete.add('l_' + name)
+        if to_delete:
+            self.writeline('del ' + ', '.join(to_delete))
 
     def function_scoping(self, node, frame, children=None,
                          find_special=True):
@@ -806,7 +821,8 @@ class CodeGenerator(NodeVisitor):
             self.visit(node.template, frame)
             self.write(', %r)' % self.name)
             self.writeline('for event in template.root_render_func('
-                           'template.new_context(context.parent, True)):')
+                           'template.new_context(context.parent, True, '
+                           'locals())):')
         else:
             self.writeline('for event in environment.get_template(', node)
             self.visit(node.template, frame)
@@ -825,7 +841,7 @@ class CodeGenerator(NodeVisitor):
         self.visit(node.template, frame)
         self.write(', %r).' % self.name)
         if node.with_context:
-            self.write('make_module(context.parent, True)')
+            self.write('make_module(context.parent, True, locals())')
         else:
             self.write('module')
         if frame.toplevel and not node.target.startswith('_'):
@@ -905,7 +921,7 @@ class CodeGenerator(NodeVisitor):
         # variables at that point.  Because loops can be nested but the loop
         # variable is a special one we have to enforce aliasing for it.
         if not node.recursive:
-            aliases = self.collect_shadowed(loop_frame, ('loop',))
+            aliases = self.push_scope(loop_frame, ('loop',))
 
         # otherwise we set up a buffer and add a function def
         else:
@@ -994,7 +1010,7 @@ class CodeGenerator(NodeVisitor):
             self.outdent()
 
         # reset the aliases if there are any.
-        self.restore_shadowed(aliases)
+        self.pop_scope(aliases, loop_frame)
 
         # if the node was recursive we have to return the buffer contents
         # and start the iteration code
@@ -1043,14 +1059,14 @@ class CodeGenerator(NodeVisitor):
     def visit_FilterBlock(self, node, frame):
         filter_frame = frame.inner()
         filter_frame.inspect(node.iter_child_nodes())
-        aliases = self.collect_shadowed(filter_frame)
+        aliases = self.push_scope(filter_frame)
         self.pull_locals(filter_frame)
         self.buffer(filter_frame)
         self.blockvisit(node.body, filter_frame)
         self.start_write(frame, node)
         self.visit_Filter(node.filter, filter_frame)
         self.end_write(frame)
-        self.restore_shadowed(aliases)
+        self.pop_scope(aliases, filter_frame)
 
     def visit_ExprStmt(self, node, frame):
         self.newline(node)
index fa8ad6c05f7d8b85f6921c4305f7bba30f906bc3..862a2477d7084e670823ca0559b953a1e8b2f937 100644 (file)
@@ -579,13 +579,13 @@ class Template(object):
             exc_type, exc_value, tb = translate_exception(sys.exc_info())
             raise exc_type, exc_value, tb
 
-    def new_context(self, vars=None, shared=False):
+    def new_context(self, vars=None, shared=False, locals=None):
         """Create a new :class:`Context` for this template.  The vars
         provided will be passed to the template.  Per default the globals
-        are added to the context, if shared is set to `True` the data
-        provided is used as parent namespace.  This is used to share the
-        same globals in multiple contexts without consuming more memory.
-        (This works because the context does not modify the parent dict)
+        are added to the context.  If shared is set to `True` the data
+        is passed as it to the context without adding the globals.
+
+        `locals` can be a dict of local variables for internal usage.
         """
         if vars is None:
             vars = {}
@@ -593,16 +593,22 @@ class Template(object):
             parent = vars
         else:
             parent = dict(self.globals, **vars)
+        if locals:
+            if shared:
+                parent = dict(parent)
+            for key, value in locals.iteritems():
+                if key[:2] == 'l_' and value is not missing:
+                    parent[key[2:]] = value
         return Context(self.environment, parent, self.name, self.blocks)
 
-    def make_module(self, vars=None, shared=False):
+    def make_module(self, vars=None, shared=False, locals=None):
         """This method works like the :attr:`module` attribute when called
         without arguments but it will evaluate the template every call
         rather then caching the template.  It's also possible to provide
         a dict which is then used as context.  The arguments are the same
         as for the :meth:`new_context` method.
         """
-        return TemplateModule(self, self.new_context(vars, shared))
+        return TemplateModule(self, self.new_context(vars, shared, locals))
 
     @property
     def module(self):
index 2afab2e9d7c1dcec73db0da7b3901d024f581c5d..bf8f56979d65d0294c5a9433688d0b6329da0e7f 100644 (file)
@@ -40,6 +40,14 @@ def test_context_include():
     assert t.render(foo=42) == '[|23]'
 
 
+def test_context_include_with_overrides():
+    env = Environment(loader=DictLoader(dict(
+        main="{% for item in [1, 2, 3] %}{% include 'item' %}{% endfor %}",
+        item="{{ item }}"
+    )))
+    assert env.get_template("main").render() == "123"
+
+
 def test_trailing_comma():
     test_env.from_string('{% from "foo" import bar, baz with context %}')
     test_env.from_string('{% from "foo" import bar, baz, with context %}')