imports and includes "with context" are passed the full context now, not only the initial one.
--HG--
branch : trunk
diff --git a/CHANGES b/CHANGES
index 3a05bca..7a5d0c2 100644
--- a/CHANGES
+++ b/CHANGES
@@ -34,6 +34,9 @@
- 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)
diff --git a/docs/templates.rst b/docs/templates.rst
index 0b68686..0c01eee 100644
--- a/docs/templates.rst
+++ b/docs/templates.rst
@@ -794,6 +794,19 @@
{% 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:
diff --git a/jinja2/compiler.py b/jinja2/compiler.py
index 0686c84..e6b2de0 100644
--- a/jinja2/compiler.py
+++ b/jinja2/compiler.py
@@ -520,23 +520,38 @@
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 @@
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 @@
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 @@
# 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 @@
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 @@
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)
diff --git a/jinja2/environment.py b/jinja2/environment.py
index fa8ad6c..862a247 100644
--- a/jinja2/environment.py
+++ b/jinja2/environment.py
@@ -579,13 +579,13 @@
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 @@
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):
diff --git a/tests/test_imports.py b/tests/test_imports.py
index 2afab2e..bf8f569 100644
--- a/tests/test_imports.py
+++ b/tests/test_imports.py
@@ -40,6 +40,14 @@
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 %}')