summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES8
-rw-r--r--jinja2/__init__.py2
-rw-r--r--jinja2/compiler.py58
-rw-r--r--jinja2/idtracking.py19
-rw-r--r--jinja2/nodes.py16
-rw-r--r--setup.py2
-rw-r--r--tests/test_ext.py23
7 files changed, 114 insertions, 14 deletions
diff --git a/CHANGES b/CHANGES
index 8e5b4ab..c37c8b0 100644
--- a/CHANGES
+++ b/CHANGES
@@ -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.
diff --git a/setup.py b/setup.py
index 79ad148..b77ff42 100644
--- a/setup.py
+++ b/setup.py
@@ -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'