From: Armin Ronacher Date: Sat, 4 Oct 2008 16:06:57 +0000 (+0200) Subject: imports and includes "with context" are passed the full context now, not only the... X-Git-Tag: 2.1~14 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=673aa88d728b1179a7f89d786e82a6b63b23be00;p=jinja2.git 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 @@ 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) 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 @@ 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: 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 @@ 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) 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 @@ 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): 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 @@ 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 %}')