Skip to content

Commit

Permalink
imports and includes "with context" are passed the full context now, …
Browse files Browse the repository at this point in the history
…not only the initial one.

--HG--
branch : trunk
  • Loading branch information
mitsuhiko committed Oct 4, 2008
1 parent 7966895 commit 673aa88
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 16 deletions.
3 changes: 3 additions & 0 deletions CHANGES
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions docs/templates.rst
Expand Up @@ -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:

Expand Down
34 changes: 25 additions & 9 deletions jinja2/compiler.py
Expand Up @@ -520,23 +520,38 @@ def pull_dependencies(self, nodes):
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):
Expand Down Expand Up @@ -806,7 +821,8 @@ def visit_Include(self, node, frame):
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)
Expand All @@ -825,7 +841,7 @@ def visit_Import(self, node, frame):
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('_'):
Expand Down Expand Up @@ -905,7 +921,7 @@ def visit_For(self, node, frame):
# 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:
Expand Down Expand Up @@ -994,7 +1010,7 @@ def visit_For(self, node, frame):
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
Expand Down Expand Up @@ -1043,14 +1059,14 @@ def visit_CallBlock(self, node, frame):
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)
Expand Down
20 changes: 13 additions & 7 deletions jinja2/environment.py
Expand Up @@ -579,30 +579,36 @@ def generate(self, *args, **kwargs):
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 = {}
if shared:
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):
Expand Down
8 changes: 8 additions & 0 deletions tests/test_imports.py
Expand Up @@ -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 %}')
Expand Down

0 comments on commit 673aa88

Please sign in to comment.