From: Armin Ronacher Date: Thu, 19 Feb 2009 14:56:53 +0000 (+0100) Subject: Added support for optional `scoped` modifier to blocks. X-Git-Tag: 2.2~39 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=74a0cd92cca22f947959c0125d59f472b8353e16;p=thirdparty%2Fjinja.git Added support for optional `scoped` modifier to blocks. --HG-- branch : trunk --- diff --git a/CHANGES b/CHANGES index c49797ba..213cd725 100644 --- a/CHANGES +++ b/CHANGES @@ -18,6 +18,7 @@ Version 2.2 - 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 fe21eefe..9fd96926 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -378,6 +378,32 @@ readability:: 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 %} +
  • {% block loop_item %}{{ item }}{% endblock %}
  • + {% endfor %} + +This example would output empty ``
  • `` 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 %} +
  • {% block loop_item scoped %}{{ item }}{% endblock %}
  • + {% 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 20ac03b3..6b9c786f 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -783,8 +783,12 @@ class CodeGenerator(NodeVisitor): 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 3c53fbd5..fcc11d2f 100644 --- a/jinja2/environment.py +++ b/jinja2/environment.py @@ -15,7 +15,7 @@ from jinja2.lexer import get_lexer, TokenStream 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 @@ class Template(object): `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 6383372b..c7858b6e 100644 --- a/jinja2/nodes.py +++ b/jinja2/nodes.py @@ -269,7 +269,7 @@ class FilterBlock(Stmt): 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 d3eb8c4b..f3de6e70 100644 --- a/jinja2/parser.py +++ b/jinja2/parser.py @@ -149,6 +149,7 @@ class Parser(object): 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 60a9035f..013d9872 100644 --- a/jinja2/runtime.py +++ b/jinja2/runtime.py @@ -17,7 +17,7 @@ from jinja2.exceptions import UndefinedError, TemplateRuntimeError, \ # 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 @@ def unicode_join(seq): 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 @@ class Context(object): 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 @@ except ImportError: 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 ed7b31a2..6a45f592 100644 --- a/tests/test_inheritance.py +++ b/tests/test_inheritance.py @@ -166,3 +166,12 @@ def test_fixed_macro_scoping_bug(): {% 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]'