Added support for optional `scoped` modifier to blocks.
--HG--
branch : trunk
diff --git a/CHANGES b/CHANGES
index c49797b..213cd72 100644
--- a/CHANGES
+++ b/CHANGES
@@ -18,6 +18,7 @@
- Fixed a bug that caused internal errors if names where used as iteration
variable and regular variable *after* the loop if that variable was unused
*before* the loop. (#331)
+- Added support for optional `scoped` modifier to blocks.
Version 2.1.1
-------------
diff --git a/docs/templates.rst b/docs/templates.rst
index fe21eef..9fd9692 100644
--- a/docs/templates.rst
+++ b/docs/templates.rst
@@ -378,6 +378,32 @@
However the name after the `endblock` word must match the block name.
+Block Nesting and Scope
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Blocks can be nested for more complex layouts. However per default blocks
+may not access variables from outer scopes::
+
+ {% for item in seq %}
+ <li>{% block loop_item %}{{ item }}{% endblock %}</li>
+ {% endfor %}
+
+This example would output empty ``<li>`` items because `item` is unavailable
+inside the block. The reason for this is that if the block is replaced by
+a child template a variable would appear that was not defined in the block or
+passed to the context.
+
+Starting with Jinja 2.2 you can explicitly specify that variables are
+available in a block by setting the block to "scoped" by adding the `scoped`
+modifier to a block declaration::
+
+ {% for item in seq %}
+ <li>{% block loop_item scoped %}{{ item }}{% endblock %}</li>
+ {% endfor %}
+
+ When overriding a block the `scoped` modifier does not have to be provided.
+
+
HTML Escaping
-------------
diff --git a/jinja2/compiler.py b/jinja2/compiler.py
index 20ac03b..6b9c786 100644
--- a/jinja2/compiler.py
+++ b/jinja2/compiler.py
@@ -783,8 +783,12 @@
self.writeline('if parent_template is None:')
self.indent()
level += 1
- self.writeline('for event in context.blocks[%r][0](context):' %
- node.name, node)
+ if node.scoped:
+ context = 'context.derived(locals())'
+ else:
+ context = 'context'
+ self.writeline('for event in context.blocks[%r][0](%s):' % (
+ node.name, context), node)
self.indent()
self.simple_write('event', frame)
self.outdent(level)
diff --git a/jinja2/environment.py b/jinja2/environment.py
index 3c53fbd..fcc11d2 100644
--- a/jinja2/environment.py
+++ b/jinja2/environment.py
@@ -15,7 +15,7 @@
from jinja2.parser import Parser
from jinja2.optimizer import optimize
from jinja2.compiler import generate
-from jinja2.runtime import Undefined, Context
+from jinja2.runtime import Undefined, new_context
from jinja2.exceptions import TemplateSyntaxError
from jinja2.utils import import_string, LRUCache, Markup, missing, \
concat, consume
@@ -646,21 +646,8 @@
`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 the parent is shared a copy should be created because
- # we don't want to modify the dict passed
- 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)
+ return new_context(self.environment, self.name, self.blocks,
+ vars, shared, self.globals, locals)
def make_module(self, vars=None, shared=False, locals=None):
"""This method works like the :attr:`module` attribute when called
diff --git a/jinja2/nodes.py b/jinja2/nodes.py
index 6383372..c7858b6 100644
--- a/jinja2/nodes.py
+++ b/jinja2/nodes.py
@@ -269,7 +269,7 @@
class Block(Stmt):
"""A node that represents a block."""
- fields = ('name', 'body')
+ fields = ('name', 'body', 'scoped')
class Include(Stmt):
diff --git a/jinja2/parser.py b/jinja2/parser.py
index d3eb8c4..f3de6e7 100644
--- a/jinja2/parser.py
+++ b/jinja2/parser.py
@@ -149,6 +149,7 @@
def parse_block(self):
node = nodes.Block(lineno=self.stream.next().lineno)
node.name = self.stream.expect('name').value
+ node.scoped = self.stream.skip_if('name:scoped')
node.body = self.parse_statements(('name:endblock',), drop_needle=True)
self.stream.skip_if('name:' + node.name)
return node
diff --git a/jinja2/runtime.py b/jinja2/runtime.py
index 60a9035..013d987 100644
--- a/jinja2/runtime.py
+++ b/jinja2/runtime.py
@@ -17,7 +17,7 @@
# these variables are exported to the template runtime
-__all__ = ['LoopContext', 'Context', 'TemplateReference', 'Macro', 'Markup',
+__all__ = ['LoopContext', 'TemplateReference', 'Macro', 'Markup',
'TemplateRuntimeError', 'missing', 'concat', 'escape',
'markup_join', 'unicode_join', 'TemplateNotFound']
@@ -42,6 +42,45 @@
return concat(imap(unicode, seq))
+def new_context(environment, template_name, blocks, vars=None,
+ shared=None, globals=None, locals=None):
+ """Internal helper to for context creation."""
+ if vars is None:
+ vars = {}
+ if shared:
+ parent = vars
+ else:
+ parent = dict(globals or (), **vars)
+ if locals:
+ # if the parent is shared a copy should be created because
+ # we don't want to modify the dict passed
+ 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(environment, parent, template_name, blocks)
+
+
+class TemplateReference(object):
+ """The `self` in templates."""
+
+ def __init__(self, context):
+ self.__context = context
+
+ def __getitem__(self, name):
+ blocks = self.__context.blocks[name]
+ wrap = self.__context.environment.autoescape and \
+ Markup or (lambda x: x)
+ return BlockReference(name, self.__context, blocks, 0)
+
+ def __repr__(self):
+ return '<%s %r>' % (
+ self.__class__.__name__,
+ self.__context.name
+ )
+
+
class Context(object):
"""The template context holds the variables of a template. It stores the
values passed to the template and also the names the template exports.
@@ -132,6 +171,11 @@
args = (__self.environment,) + args
return __obj(*args, **kwargs)
+ def derived(self, locals=None):
+ """Internal helper function to create a derived context."""
+ return new_context(self.environment, self.name, self.blocks,
+ self.parent, True, None, locals)
+
def _all(meth):
proxy = lambda self: getattr(self.get_all(), meth)()
proxy.__doc__ = getattr(dict, meth).__doc__
@@ -174,25 +218,6 @@
pass
-class TemplateReference(object):
- """The `self` in templates."""
-
- def __init__(self, context):
- self.__context = context
-
- def __getitem__(self, name):
- blocks = self.__context.blocks[name]
- wrap = self.__context.environment.autoescape and \
- Markup or (lambda x: x)
- return BlockReference(name, self.__context, blocks, 0)
-
- def __repr__(self):
- return '<%s %r>' % (
- self.__class__.__name__,
- self.__context.name
- )
-
-
class BlockReference(object):
"""One block on a template reference."""
diff --git a/tests/test_inheritance.py b/tests/test_inheritance.py
index ed7b31a..6a45f59 100644
--- a/tests/test_inheritance.py
+++ b/tests/test_inheritance.py
@@ -166,3 +166,12 @@
{% block content %} {% endblock %}
'''
})).get_template("test.html").render().split() == [u'outer_box', u'my_macro']
+
+
+def test_scoped_block():
+ env = Environment(loader=DictLoader({
+ 'master.html': '{% for item in seq %}[{% block item scoped %}'
+ '{% endblock %}]{% endfor %}'
+ }))
+ t = env.from_string('{% extends "master.html" %}{% block item %}{{ item }}{% endblock %}')
+ assert t.render(seq=range(5)) == '[0][1][2][3][4]'