added a deprecation warning for a variable assignment, scope bug
authorArmin Ronacher <armin.ronacher@active-4.com>
Fri, 18 Sep 2009 17:32:46 +0000 (19:32 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Fri, 18 Sep 2009 17:32:46 +0000 (19:32 +0200)
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

.hgignore
CHANGES
docs/faq.rst
jinja2/compiler.py
jinja2/runtime.py
setup.py
tests/test_old_bugs.py

index d5acae402e6e52c3976471438824d94717442fc3..3204fc7f5d80dd888950026f85016ebb80e98590 100644 (file)
--- 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 f9f94990b806d83ee46924c66e03d103300abc91..97b9b54d6fe5d97e5a0f0f19904c712b6cce4a9c 100644 (file)
--- 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
 -------------
index 0e3bf5a214a4be844bdfbe526cbeab183208125e..e8ddc6a42143c7e748494e54662d0400c690ed38 100644 (file)
@@ -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/
index 33aadc3348df102593bddf0da9a1154e2f0e709f..93ae14b6aa4cdef234d5d727d49177debd47d874 100644 (file)
@@ -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()
index f8c251bdd7ac4d3e70060c93c79c09f76aded15f..4a61bf9e5b88ae443e051957fa808d38a60e85d7 100644 (file)
@@ -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
index 62cbcf5c2013cff81913b90f71719d2c9bf0fcb1..09998a49744503a39039855b1ad73427a471379b 100644 (file)
--- 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',
index bea07dc2ff30eabd7cd3786312d911f5b9d51fc9..c4468f12e93bbedbb38dc73e139095ee87719dd5 100644 (file)
@@ -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'