diff options
-rw-r--r-- | CHANGES | 8 | ||||
-rw-r--r-- | jinja2/__init__.py | 2 | ||||
-rw-r--r-- | jinja2/compiler.py | 58 | ||||
-rw-r--r-- | jinja2/idtracking.py | 19 | ||||
-rw-r--r-- | jinja2/nodes.py | 16 | ||||
-rw-r--r-- | setup.py | 2 | ||||
-rw-r--r-- | tests/test_ext.py | 23 |
7 files changed, 114 insertions, 14 deletions
@@ -1,6 +1,14 @@ Jinja2 Changelog ================ +Version 2.10 +------------ +(feature release, release date to be decided) + +- Added a new extension node called `OverlayScope` which can be used to + create an unoptimized scope that will look up all variables from a + derived context. + Version 2.9.3 ------------- (bugfix release, released on January 8th 2017) diff --git a/jinja2/__init__.py b/jinja2/__init__.py index 63d104b..ccc5d18 100644 --- a/jinja2/__init__.py +++ b/jinja2/__init__.py @@ -27,7 +27,7 @@ :license: BSD, see LICENSE for more details. """ __docformat__ = 'restructuredtext en' -__version__ = '2.9.4.dev' +__version__ = '2.10.dev' # high level interface from jinja2.environment import Environment, Template diff --git a/jinja2/compiler.py b/jinja2/compiler.py index cdfe38e..6c2f588 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -130,9 +130,10 @@ class MacroRef(object): class Frame(object): """Holds compile time information for us.""" - def __init__(self, eval_ctx, parent=None): + def __init__(self, eval_ctx, parent=None, level=None): self.eval_ctx = eval_ctx - self.symbols = Symbols(parent and parent.symbols or None) + self.symbols = Symbols(parent and parent.symbols or None, + level=level) # a toplevel frame is the root + soft frames such as if conditions. self.toplevel = False @@ -168,8 +169,10 @@ class Frame(object): rv.symbols = self.symbols.copy() return rv - def inner(self): + def inner(self, isolated=False): """Return an inner frame.""" + if isolated: + return Frame(self.eval_ctx, level=self.symbols.level + 1) return Frame(self.eval_ctx, self) def soft(self): @@ -302,6 +305,9 @@ class CodeGenerator(NodeVisitor): # Tracks parameter definition blocks self._param_def_block = [] + # Tracks the current context. + self._context_reference_stack = ['context'] + # -- Various compilation helpers def fail(self, msg, lineno): @@ -473,8 +479,8 @@ class CodeGenerator(NodeVisitor): if action == VAR_LOAD_PARAMETER: pass elif action == VAR_LOAD_RESOLVE: - self.writeline('%s = resolve(%r)' % - (target, param)) + self.writeline('%s = %s(%r)' % + (target, self.get_resolve_func(), param)) elif action == VAR_LOAD_ALIAS: self.writeline('%s = %s' % (target, param)) elif action == VAR_LOAD_UNDEFINED: @@ -626,6 +632,27 @@ class CodeGenerator(NodeVisitor): if self._param_def_block: self._param_def_block[-1].discard(target) + def push_context_reference(self, target): + self._context_reference_stack.append(target) + + def pop_context_reference(self): + self._context_reference_stack.pop() + + def get_context_ref(self): + return self._context_reference_stack[-1] + + def get_resolve_func(self): + target = self._context_reference_stack[-1] + if target == 'context': + return 'resolve' + return '%s.resolve' % target + + def derive_context(self, frame): + return '%s.derived(%s)' % ( + self.get_context_ref(), + self.dump_local_context(frame), + ) + def parameter_is_undeclared(self, target): """Checks if a given target is an undeclared parameter.""" if not self._param_def_block: @@ -793,8 +820,11 @@ class CodeGenerator(NodeVisitor): self.writeline('if parent_template is None:') self.indent() level += 1 - context = node.scoped and ( - 'context.derived(%s)' % self.dump_local_context(frame)) or 'context' + + if node.scoped: + context = self.derive_context(frame) + else: + context = self.get_context_ref() if supports_yield_from and not self.environment.is_async and \ frame.buffer is None: @@ -1631,6 +1661,20 @@ class CodeGenerator(NodeVisitor): self.blockvisit(node.body, scope_frame) self.leave_frame(scope_frame) + def visit_OverlayScope(self, node, frame): + ctx = self.temporary_identifier() + self.writeline('%s = %s' % (ctx, self.derive_context(frame))) + self.writeline('%s.vars = ' % ctx) + self.visit(node.context, frame) + self.push_context_reference(ctx) + + scope_frame = frame.inner(isolated=True) + scope_frame.symbols.analyze_node(node) + self.enter_frame(scope_frame) + self.blockvisit(node.body, scope_frame) + self.leave_frame(scope_frame) + self.pop_context_reference() + def visit_EvalContextModifier(self, node, frame): for keyword in node.options: self.writeline('context.eval_ctx.%s = ' % keyword.key) diff --git a/jinja2/idtracking.py b/jinja2/idtracking.py index 433b92c..276e390 100644 --- a/jinja2/idtracking.py +++ b/jinja2/idtracking.py @@ -24,11 +24,13 @@ def symbols_for_node(node, parent_symbols=None): class Symbols(object): - def __init__(self, parent=None): - if parent is None: - self.level = 0 - else: - self.level = parent.level + 1 + def __init__(self, parent=None, level=None): + if level is None: + if parent is None: + level = 0 + else: + level = parent.level + 1 + self.level = level self.parent = parent self.refs = {} self.loads = {} @@ -167,6 +169,10 @@ class RootVisitor(NodeVisitor): for child in node.iter_child_nodes(exclude=('call',)): self.sym_visitor.visit(child) + def visit_OverlayScope(self, node, **kwargs): + for child in node.body: + self.sym_visitor.visit(child) + def visit_For(self, node, for_branch='body', **kwargs): if node.test is not None: self.sym_visitor.visit(node.test) @@ -268,3 +274,6 @@ class FrameSymbolVisitor(NodeVisitor): def visit_Block(self, node, **kwargs): """Stop visiting at blocks.""" + + def visit_OverlayScope(self, node, **kwargs): + """Do not visit into overlay scopes.""" diff --git a/jinja2/nodes.py b/jinja2/nodes.py index 2c6a296..5deac46 100644 --- a/jinja2/nodes.py +++ b/jinja2/nodes.py @@ -914,6 +914,22 @@ class Scope(Stmt): fields = ('body',) +class OverlayScope(Stmt): + """An overlay scope for extensions. This is a largely unoptimized scope + that however can be used to introduce completely arbitrary variables into + a sub scope from a dictionary or dictionary like object. The `context` + field has to evaluate to a dictionary object. + + Example usage:: + + OverlayScope(context=self.call_method('get_context'), + body=[...]) + + .. versionadded:: 2.10 + """ + fields = ('context', 'body') + + class EvalContextModifier(Stmt): """Modifies the eval context. For each option that should be modified, a :class:`Keyword` has to be added to the :attr:`options` list. @@ -40,7 +40,7 @@ from setuptools import setup setup( name='Jinja2', - version='2.9.4.dev', + version='2.10.dev', url='http://jinja.pocoo.org/', license='BSD', author='Armin Ronacher', diff --git a/tests/test_ext.py b/tests/test_ext.py index 1301d22..8966107 100644 --- a/tests/test_ext.py +++ b/tests/test_ext.py @@ -495,3 +495,26 @@ class TestAutoEscape(object): autoescape=True) pysource = env.compile(tmplsource, raw=True) assert '<testing>\\n' in pysource + + def test_overlay_scopes(self): + class MagicScopeExtension(Extension): + tags = set(['overlay']) + def parse(self, parser): + node = nodes.OverlayScope(lineno=next(parser.stream).lineno) + node.body = list(parser.parse_statements(('name:endoverlay',), + drop_needle=True)) + node.context = self.call_method('get_scope') + return node + def get_scope(self): + return {'x': [1, 2, 3]} + + env = Environment(extensions=[MagicScopeExtension]) + + tmpl = env.from_string(''' + {{- x }}|{% set z = 99 %} + {%- overlay %} + {{- y }}|{{ z }}|{% for item in x %}[{{ item }}]{% endfor %} + {%- endoverlay %}| + {{- x -}} + ''') + assert tmpl.render(x=42, y=23) == '42|23|99|[1][2][3]|42' |