From: Armin Ronacher Date: Fri, 18 Sep 2009 17:32:46 +0000 (+0200) Subject: added a deprecation warning for a variable assignment, scope bug X-Git-Tag: 2.3~51 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=2b48839fb65f0ce5276c3de4ad30a705e89a3162;p=jinja2.git added a deprecation warning for a variable assignment, scope bug that exists since 2.0, code could depend on. See :ref:`jinja-scoping-bug` for more information on this problem. Tip is 2.3 as this will be the next release (will happen soon!) --HG-- branch : trunk --- diff --git a/.hgignore b/.hgignore index d5acae4..3204fc7 100644 --- a/.hgignore +++ b/.hgignore @@ -3,7 +3,7 @@ ^jinja2/.*\.so$ ^docs/_build ^(build|dist|Jinja2\.egg-info)/ -\.py[co]$ +\.py[cod]$ \$py\.class$ \.DS_Store$ ^j?env/ diff --git a/CHANGES b/CHANGES index f9f9499..97b9b54 100644 --- a/CHANGES +++ b/CHANGES @@ -1,12 +1,15 @@ Jinja2 Changelog ================ -Version 2.2.2 -------------- -(bugfix release, to be released soon) +Version 2.3 +----------- +(codename Gnok, release date to be selected.) - fixes issue with code generator that causes unbound variables to be generated if set was used in if-blocks. +- added a deprecation warning for a variable assignment, scope bug + that exists since 2.0, code could depend on. See :ref:`jinja-scoping-bug` + for more information on this problem. Version 2.2.1 ------------- diff --git a/docs/faq.rst b/docs/faq.rst index 0e3bf5a..e8ddc6a 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -152,4 +152,41 @@ harder to maintain the code for older Python versions. If you really need Python 2.3 support you either have to use `Jinja 1`_ or other templating engines that still support 2.3. +.. _jinja-scoping-bug: + +Scoping Bug in Jinja2 +--------------------- + +Jinja2 currently has a scoping bug that causes confusing behavior. If you +have a layout template that defines a macro, and a child template that +does the same thing, it will see the macro from the layout template: + +layout.tmpl: + +.. sourcecode:: jinja + + {% macro foo() %}LAYOUT{% endmacro %} + {% block body %}{% endblock %} + +child.tmpl: + +.. sourcecode:: jinja + + {% extends 'layout.tmpl' %} + {% macro foo() %}CHILD{% endmacro %} + {% block body %}{{ foo() }}{% endblock %} + +This will print ``LAYOUT`` in Jinja2 versions older than 2.5. Starting +with Jinja 2.2.2, there will however be a deprecation warning for this +behavior. Starting with Jinja 2.5, this behavior will change so that +``CHILD`` is printed instead. + +The reason why it was not changed right away is that some templates could +depend on macros or variables defined in the layout template to be +available in a child template. This however was undocumented behavior and +is considered a bug. + +If you depend on this behavior, move your macros into a different file and +import it into the layout and child template. + .. _Jinja 1: http://jinja.pocoo.org/1/ diff --git a/jinja2/compiler.py b/jinja2/compiler.py index 33aadc3..93ae14b 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -725,6 +725,9 @@ class CodeGenerator(NodeVisitor): # overhead by just not processing any inheritance code. have_extends = node.find(nodes.Extends) is not None + # are there any block tags? If yes, we need a copy of the scope. + have_blocks = node.find(nodes.Block) is not None + # find all blocks for block in node.find_all(nodes.Block): if block.name in self.blocks: @@ -757,6 +760,8 @@ class CodeGenerator(NodeVisitor): self.indent() if have_extends: self.writeline('parent_template = None') + if have_blocks: + self.writeline('block_context = context._block()') if 'self' in find_undeclared(node.body, ('self',)): frame.identifiers.add_special('self') self.writeline('l_self = TemplateReference(context)') @@ -789,6 +794,8 @@ class CodeGenerator(NodeVisitor): if 'self' in undeclared: block_frame.identifiers.add_special('self') self.writeline('l_self = TemplateReference(context)') + if block.find(nodes.Block) is not None: + self.writeline('block_context = context._block(%r)' % name) if 'super' in undeclared: block_frame.identifiers.add_special('super') self.writeline('l_super = context.super(%r, ' @@ -819,9 +826,9 @@ class CodeGenerator(NodeVisitor): self.indent() level += 1 if node.scoped: - context = 'context.derived(locals())' + context = 'block_context.derived(locals())' else: - context = 'context' + context = 'block_context' self.writeline('for event in context.blocks[%r][0](%s):' % ( node.name, context), node) self.indent() diff --git a/jinja2/runtime.py b/jinja2/runtime.py index f8c251b..4a61bf9 100644 --- a/jinja2/runtime.py +++ b/jinja2/runtime.py @@ -185,6 +185,16 @@ class Context(object): context.blocks.update((k, list(v)) for k, v in self.blocks.iteritems()) return context + def _block(self, block=None): + """Creates a context that is used for block execution. Currently this + returns a special `_BlockContext` that warns about changed behavior. + In Jinja 2.5, this will instead just return a new context with the same + resolve behavior. Do not call from anywhere but the generated code! + + :private: + """ + return _BlockContext(self, block) + def _all(meth): proxy = lambda self: getattr(self.get_all(), meth)() proxy.__doc__ = getattr(dict, meth).__doc__ @@ -222,6 +232,39 @@ class Context(object): ) +class _BlockContext(Context): + """Implements a deprecation warning for the changed block assignments.""" + __slots__ = ('real_context', 'block_name') + + def __init__(self, real_context, block_name): + super(_BlockContext, self).__init__(real_context.environment, + real_context.parent, + real_context.name, {}) + self.vars = dict(real_context.vars) + self.exported_vars = set(real_context.exported_vars) + self.blocks = real_context.blocks + self.real_context = real_context + self.block_name = block_name + + def resolve(self, key): + self_rv = super(_BlockContext, self).resolve(key) + base_rv = self.real_context.resolve(key) + if self_rv != base_rv and not isinstance(base_rv, Undefined): + from warnings import warn + if self.block_name is not None: + detail = 'accessing %r from inside block %r' \ + % (key, self.block_name) + else: + detail = 'accessing %r toplevel' % key + detail += ', in template %s' % self.name + warn(DeprecationWarning('variables set in a base template ' + 'will no longer leak into the child ' + 'context in future versions. Happened ' + 'when ' + detail)) + return base_rv + return self_rv + + # register the context as mapping if possible try: from collections import Mapping diff --git a/setup.py b/setup.py index 62cbcf5..09998a4 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ from distutils.errors import CCompilerError, DistutilsPlatformError setup( name='Jinja2', - version='2.2.2', + version='2.3', url='http://jinja.pocoo.org/', license='BSD', author='Armin Ronacher', diff --git a/tests/test_old_bugs.py b/tests/test_old_bugs.py index bea07dc..c4468f1 100644 --- a/tests/test_old_bugs.py +++ b/tests/test_old_bugs.py @@ -12,6 +12,7 @@ from jinja2 import Environment, DictLoader, TemplateSyntaxError env = Environment() +from nose import SkipTest from nose.tools import assert_raises @@ -75,3 +76,15 @@ def test_partial_conditional_assignments(): tmpl = env.from_string('{% if b %}{% set a = 42 %}{% endif %}{{ a }}') assert tmpl.render(a=23) == '23' assert tmpl.render(b=True) == '42' + + +def test_local_macros_first(): + raise SkipTest('Behavior will change in 2.3') + env = Environment(loader=DictLoader({ + 'layout.html': ('{% macro foo() %}LAYOUT{% endmacro %}' + '{% block body %}{% endblock %}'), + 'child.html': ('{% extends "layout.html" %}' + '{% macro foo() %}CHILD{% endmacro %}' + '{% block body %}{{ foo() }}{% endblock %}') + })) + assert env.get_template('child.html').render() == 'CHILD'