all unittests pass, the special and dependency lookups have their own visitors now, with `self` one can get a reference to the current template and render blocks multiple times.
--HG--
branch : trunk
diff --git a/jinja2/compiler.py b/jinja2/compiler.py
index a1ffdec..8c699f5 100644
--- a/jinja2/compiler.py
+++ b/jinja2/compiler.py
@@ -32,7 +32,6 @@
'notin': 'not in'
}
-
try:
exec '(0 if 0 else 0)'
except SyntaxError:
@@ -71,6 +70,19 @@
return False
+def find_undeclared(nodes, names):
+ """Check if the names passed are accessed undeclared. The return value
+ is a set of all the undeclared names from the sequence of names found.
+ """
+ visitor = UndeclaredNameVisitor(names)
+ try:
+ for node in nodes:
+ visitor.visit(node)
+ except VisitorExit:
+ pass
+ return visitor.undeclared
+
+
class Identifiers(object):
"""Tracks the status of identifiers in frames."""
@@ -93,10 +105,6 @@
# names that are declared by parameters
self.declared_parameter = set()
- # filters/tests that are referenced
- self.filters = set()
- self.tests = set()
-
def add_special(self, name):
"""Register a special name like `loop`."""
self.undeclared.discard(name)
@@ -136,10 +144,6 @@
# buffer.
self.buffer = None
- # if a frame has name_overrides, all read access to a name in this
- # dict is redirected to a string expression.
- self.name_overrides = {}
-
# the name of the block we're in, otherwise None.
self.block = parent and parent.block or None
@@ -157,28 +161,21 @@
self.identifiers.declared
)
self.buffer = parent.buffer
- self.name_overrides = parent.name_overrides.copy()
def copy(self):
"""Create a copy of the current one."""
rv = copy(self)
rv.identifiers = copy(self.identifiers)
- rv.name_overrides = self.name_overrides.copy()
return rv
- def inspect(self, nodes, with_depenencies=False, hard_scope=False):
+ def inspect(self, nodes, hard_scope=False):
"""Walk the node and check for identifiers. If the scope is hard (eg:
enforce on a python level) overrides from outer scopes are tracked
differently.
-
- Per default filters and tests (dependencies) are not tracked. That's
- the case because filters and tests are absolutely immutable and so we
- can savely use them in closures too. The `Template` and `Block`
- visitor visits the frame with dependencies to collect them.
"""
visitor = FrameIdentifierVisitor(self.identifiers, hard_scope)
for node in nodes:
- visitor.visit(node, True, with_depenencies)
+ visitor.visit(node)
def inner(self):
"""Return an inner frame."""
@@ -194,6 +191,51 @@
return rv
+class VisitorExit(RuntimeError):
+ """Exception used by the `UndeclaredNameVisitor` to signal a stop."""
+
+
+class DependencyFinderVisitor(NodeVisitor):
+ """A visitor that collects filter and test calls."""
+
+ def __init__(self):
+ self.filters = set()
+ self.tests = set()
+
+ def visit_Filter(self, node):
+ self.generic_visit(node)
+ self.filters.add(node.name)
+
+ def visit_Test(self, node):
+ self.generic_visit(node)
+ self.tests.add(node.name)
+
+ def visit_Block(self, node):
+ """Stop visiting at blocks."""
+
+
+class UndeclaredNameVisitor(NodeVisitor):
+ """A visitor that checks if a name is accessed without being
+ declared. This is different from the frame visitor as it will
+ not stop at closure frames.
+ """
+
+ def __init__(self, names):
+ self.names = set(names)
+ self.undeclared = set()
+
+ def visit_Name(self, node):
+ if node.ctx == 'load' and node.name in self.names:
+ self.undeclared.add(node.name)
+ if self.undeclared == self.names:
+ raise VisitorExit()
+ else:
+ self.names.discard(node.name)
+
+ def visit_Block(self, node):
+ """Stop visiting a blocks."""
+
+
class FrameIdentifierVisitor(NodeVisitor):
"""A visitor for `Frame.inspect`."""
@@ -201,63 +243,50 @@
self.identifiers = identifiers
self.hard_scope = hard_scope
- def visit_Name(self, node, visit_ident, visit_deps):
+ def visit_Name(self, node):
"""All assignments to names go through this function."""
- if visit_ident:
- if node.ctx in ('store', 'param'):
- self.identifiers.declared_locally.add(node.name)
- elif node.ctx == 'load' and not \
- self.identifiers.is_declared(node.name, self.hard_scope):
- self.identifiers.undeclared.add(node.name)
-
- def visit_Filter(self, node, visit_ident, visit_deps):
- if visit_deps:
- self.generic_visit(node, visit_ident, True)
- self.identifiers.filters.add(node.name)
-
- def visit_Test(self, node, visit_ident, visit_deps):
- if visit_deps:
- self.generic_visit(node, visit_ident, True)
- self.identifiers.tests.add(node.name)
-
- def visit_Macro(self, node, visit_ident, visit_deps):
- if visit_ident:
+ if node.ctx in ('store', 'param'):
self.identifiers.declared_locally.add(node.name)
+ elif node.ctx == 'load' and not \
+ self.identifiers.is_declared(node.name, self.hard_scope):
+ self.identifiers.undeclared.add(node.name)
- def visit_Import(self, node, visit_ident, visit_deps):
- if visit_ident:
- self.generic_visit(node, True, visit_deps)
- self.identifiers.declared_locally.add(node.target)
+ def visit_Macro(self, node):
+ self.generic_visit(node)
+ self.identifiers.declared_locally.add(node.name)
- def visit_FromImport(self, node, visit_ident, visit_deps):
- if visit_ident:
- self.generic_visit(node, True, visit_deps)
- for name in node.names:
- if isinstance(name, tuple):
- self.identifiers.declared_locally.add(name[1])
- else:
- self.identifiers.declared_locally.add(name)
+ def visit_Import(self, node):
+ self.generic_visit(node)
+ self.identifiers.declared_locally.add(node.target)
- def visit_Assign(self, node, visit_ident, visit_deps):
+ def visit_FromImport(self, node):
+ self.generic_visit(node)
+ for name in node.names:
+ if isinstance(name, tuple):
+ self.identifiers.declared_locally.add(name[1])
+ else:
+ self.identifiers.declared_locally.add(name)
+
+ def visit_Assign(self, node):
"""Visit assignments in the correct order."""
- self.visit(node.node, visit_ident, visit_deps)
- self.visit(node.target, visit_ident, visit_deps)
+ self.visit(node.node)
+ self.visit(node.target)
- def visit_For(self, node, visit_ident, visit_deps):
+ def visit_For(self, node):
"""Visiting stops at for blocks. However the block sequence
is visited as part of the outer scope.
"""
- if visit_ident:
- self.visit(node.iter, True, visit_deps)
- if visit_deps:
- for child in node.iter_child_nodes(exclude=('iter',)):
- self.visit(child, False, True)
+ self.visit(node.iter)
- def ident_stop(self, node, visit_ident, visit_deps):
- if visit_deps:
- self.generic_visit(node, False, True)
- visit_CallBlock = visit_FilterBlock = ident_stop
- visit_Block = lambda s, n, a, b: None
+ def visit_CallBlock(self, node):
+ for child in node.iter_child_nodes(exclude=('body',)):
+ self.visit(child)
+
+ def visit_FilterBlock(self, node):
+ self.visit(node.filter)
+
+ def visit_Block(self, node):
+ """Stop visiting at blocks."""
class CompilerExit(Exception):
@@ -325,15 +354,11 @@
"""Outdent by step."""
self._indentation -= step
- def blockvisit(self, nodes, frame, indent=True, force_generator=True):
- """Visit a list of nodes as block in a frame. Per default the
- code is indented, but this can be disabled by setting the indent
- parameter to False. If the current frame is no buffer a dummy
- ``if 0: yield None`` is written automatically unless the
- force_generator parameter is set to False.
+ def blockvisit(self, nodes, frame, force_generator=True):
+ """Visit a list of nodes as block in a frame. If the current frame
+ is no buffer a dummy ``if 0: yield None`` is written automatically
+ unless the force_generator parameter is set to False.
"""
- if indent:
- self.indent()
if frame.buffer is None and force_generator:
self.writeline('if 0: yield None')
try:
@@ -341,8 +366,6 @@
self.visit(node, frame)
except CompilerExit:
pass
- if indent:
- self.outdent()
def write(self, x):
"""Write a string into the output stream."""
@@ -423,7 +446,6 @@
self.write(', ')
if extra_kwargs is not None:
for key, value in extra_kwargs.iteritems():
- touch_comma()
self.write('%r: %s, ' % (key, value))
if node.dyn_kwargs is not None:
self.write('}, **')
@@ -437,21 +459,20 @@
self.write('**')
self.visit(node.dyn_kwargs, frame)
- def pull_locals(self, frame, indent=True):
- """Pull all the references identifiers into the local scope.
- This affects regular names, filters and tests. If indent is
- set to False, no automatic indentation will take place.
- """
- if indent:
- self.indent()
+ def pull_locals(self, frame):
+ """Pull all the references identifiers into the local scope."""
for name in frame.identifiers.undeclared:
self.writeline('l_%s = context[%r]' % (name, name))
- for name in frame.identifiers.filters:
+
+ def pull_dependencies(self, nodes):
+ """Pull all the dependencies."""
+ visitor = DependencyFinderVisitor()
+ for node in nodes:
+ visitor.visit(node)
+ for name in visitor.filters:
self.writeline('f_%s = environment.filters[%r]' % (name, name))
- for name in frame.identifiers.tests:
+ for name in visitor.tests:
self.writeline('t_%s = environment.tests[%r]' % (name, name))
- if indent:
- self.outdent()
def collect_shadowed(self, frame):
"""This function returns all the shadowed variables in a dict
@@ -467,7 +488,7 @@
self.writeline('%s = l_%s' % (ident, name))
return aliases
- def function_scoping(self, node, frame):
+ def function_scoping(self, node, frame, children=None):
"""In Jinja a few statements require the help of anonymous
functions. Those are currently macros and call blocks and in
the future also recursive loops. As there is currently
@@ -479,8 +500,12 @@
This will return the modified frame.
"""
+ # we have to iterate twice over it, make sure that works
+ if children is None:
+ children = node.iter_child_nodes()
+ children = list(children)
func_frame = frame.inner()
- func_frame.inspect(node.iter_child_nodes(), hard_scope=True)
+ func_frame.inspect(children, hard_scope=True)
# variables that are undeclared (accessed before declaration) and
# declared locally *and* part of an outside scope raise a template
@@ -511,15 +536,17 @@
func_frame.accesses_caller = False
func_frame.arguments = args = ['l_' + x.name for x in node.args]
- if 'caller' in func_frame.identifiers.undeclared:
+ undeclared = find_undeclared(children, ('caller', 'kwargs', 'varargs'))
+
+ if 'caller' in undeclared:
func_frame.accesses_caller = True
func_frame.identifiers.add_special('caller')
args.append('l_caller')
- if 'kwargs' in func_frame.identifiers.undeclared:
+ if 'kwargs' in undeclared:
func_frame.accesses_kwargs = True
func_frame.identifiers.add_special('kwargs')
args.append('l_kwargs')
- if 'varargs' in func_frame.identifiers.undeclared:
+ if 'varargs' in undeclared:
func_frame.accesses_varargs = True
func_frame.identifiers.add_special('varargs')
args.append('l_varargs')
@@ -547,18 +574,20 @@
# generate the root render function.
self.writeline('def root(context, environment=environment):', extra=1)
- if have_extends:
- self.indent()
- self.writeline('parent_template = None')
- self.outdent()
# process the root
frame = Frame()
- frame.inspect(node.body, with_depenencies=True)
+ frame.inspect(node.body)
frame.toplevel = frame.rootlevel = True
self.indent()
- self.pull_locals(frame, indent=False)
- self.blockvisit(node.body, frame, indent=False)
+ if have_extends:
+ self.writeline('parent_template = None')
+ self.pull_locals(frame)
+ self.pull_dependencies(node.body)
+ if 'self' in find_undeclared(node.body, ('self',)):
+ frame.identifiers.add_special('self')
+ self.writeline('l_self = TemplateReference(context)')
+ self.blockvisit(node.body, frame)
self.outdent()
# make sure that the parent root is called.
@@ -576,15 +605,23 @@
# at this point we now have the blocks collected and can visit them too.
for name, block in self.blocks.iteritems():
block_frame = Frame()
- block_frame.inspect(block.body, with_depenencies=True)
+ block_frame.inspect(block.body)
block_frame.block = name
- block_frame.identifiers.add_special('super')
- block_frame.name_overrides['super'] = 'context.super(%r, ' \
- 'block_%s)' % (name, name)
self.writeline('def block_%s(context, environment=environment):'
% name, block, 1)
+ self.indent()
+ undeclared = find_undeclared(block.body, ('self', 'super'))
+ if 'self' in undeclared:
+ block_frame.identifiers.add_special('self')
+ self.writeline('l_self = TemplateReference(context)')
+ if 'super' in undeclared:
+ block_frame.identifiers.add_special('super')
+ self.writeline('l_super = context.super(%r, '
+ 'block_%s)' % (name, name))
self.pull_locals(block_frame)
+ self.pull_dependencies(block.body)
self.blockvisit(block.body, block_frame)
+ self.outdent()
self.writeline('blocks = {%s}' % ', '.join('%r: block_%s' % (x, x)
for x in self.blocks),
@@ -606,7 +643,8 @@
self.writeline('if parent_template is None:')
self.indent()
level += 1
- self.writeline('for event in context.blocks[%r][-1](context):' % node.name)
+ self.writeline('for event in context.blocks[%r][-1](context):' %
+ node.name, node)
self.indent()
if frame.buffer is None:
self.writeline('yield event')
@@ -666,7 +704,7 @@
self.visit(node.template, frame)
self.write(', %r)' % self.name)
self.writeline('for event in included_template.root_render_func('
- 'included_template.new_context(context.get_root())):')
+ 'included_template.new_context(context.parent, True)):')
self.indent()
if frame.buffer is None:
self.writeline('yield event')
@@ -682,7 +720,7 @@
self.write('environment.get_template(')
self.visit(node.template, frame)
self.write(', %r).include(context)' % self.name)
- if frame.toplevel:
+ if frame.toplevel and not node.target.startswith('__'):
self.writeline('context.exported_vars.discard(%r)' % node.target)
def visit_FromImport(self, node, frame):
@@ -707,7 +745,8 @@
self.outdent()
if frame.toplevel:
self.writeline('context.vars[%r] = l_%s' % (alias, alias))
- self.writeline('context.exported_vars.discard(%r)' % alias)
+ if not alias.startswith('__'):
+ self.writeline('context.exported_vars.discard(%r)' % alias)
def visit_For(self, node, frame):
loop_frame = frame.inner()
@@ -718,7 +757,7 @@
loop_frame.identifiers.add_special('loop')
aliases = self.collect_shadowed(loop_frame)
- self.pull_locals(loop_frame, indent=False)
+ self.pull_locals(loop_frame)
if node.else_:
self.writeline('l_loop = None')
@@ -747,7 +786,7 @@
self.visit(node.iter, loop_frame)
self.write(' if (')
test_frame = loop_frame.copy()
- test_frame.name_overrides['loop'] = parent_loop
+ self.writeline('l_loop = ' + parent_loop)
self.visit(node.test, test_frame)
self.write('))')
@@ -766,14 +805,16 @@
self.writeline('continue')
self.outdent(2)
+ self.indent()
self.blockvisit(node.body, loop_frame, force_generator=True)
+ self.outdent()
if node.else_:
self.writeline('if l_loop is None:')
self.indent()
self.writeline('l_loop = ' + parent_loop)
- self.outdent()
self.blockvisit(node.else_, loop_frame, force_generator=False)
+ self.outdent()
# reset the aliases if there are any.
for name, alias in aliases.iteritems():
@@ -784,10 +825,14 @@
self.writeline('if ', node)
self.visit(node.test, if_frame)
self.write(':')
+ self.indent()
self.blockvisit(node.body, if_frame)
+ self.outdent()
if node.else_:
self.writeline('else:')
+ self.indent()
self.blockvisit(node.else_, if_frame)
+ self.outdent()
def visit_Macro(self, node, frame):
macro_frame = self.function_scoping(node, frame)
@@ -795,14 +840,15 @@
self.writeline('def macro(%s):' % ', '.join(args), node)
macro_frame.buffer = buf = self.temporary_identifier()
self.indent()
- self.pull_locals(macro_frame, indent=False)
+ self.pull_locals(macro_frame)
self.writeline('%s = []' % buf)
- self.blockvisit(node.body, macro_frame, indent=False)
+ self.blockvisit(node.body, macro_frame)
self.writeline("return Markup(concat(%s))" % buf)
self.outdent()
self.newline()
if frame.toplevel:
- self.write('context.exported_vars.add(%r)' % node.name)
+ if not node.name.startswith('__'):
+ self.write('context.exported_vars.add(%r)' % node.name)
self.writeline('context.vars[%r] = ' % node.name)
arg_tuple = ', '.join(repr(x.name) for x in node.args)
if len(node.args) == 1:
@@ -819,14 +865,15 @@
))
def visit_CallBlock(self, node, frame):
- call_frame = self.function_scoping(node, frame)
+ call_frame = self.function_scoping(node, frame, node.iter_child_nodes
+ (exclude=('call',)))
args = call_frame.arguments
self.writeline('def call(%s):' % ', '.join(args), node)
call_frame.buffer = buf = self.temporary_identifier()
self.indent()
- self.pull_locals(call_frame, indent=False)
+ self.pull_locals(call_frame)
self.writeline('%s = []' % buf)
- self.blockvisit(node.body, call_frame, indent=False)
+ self.blockvisit(node.body, call_frame)
self.writeline("return Markup(concat(%s))" % buf)
self.outdent()
arg_tuple = ', '.join(repr(x.name) for x in node.args)
@@ -835,7 +882,7 @@
self.writeline('caller = Macro(environment, call, None, (%s), (' %
arg_tuple)
for arg in node.defaults:
- self.visit(arg)
+ self.visit(arg, call_frame)
self.write(', ')
self.write('), %s, %s, 0)' % (
call_frame.accesses_kwargs and '1' or '0',
@@ -855,7 +902,7 @@
filter_frame.inspect(node.iter_child_nodes())
aliases = self.collect_shadowed(filter_frame)
- self.pull_locals(filter_frame, indent=False)
+ self.pull_locals(filter_frame)
filter_frame.buffer = buf = self.temporary_identifier()
self.writeline('%s = []' % buf, node)
@@ -985,17 +1032,12 @@
if frame.toplevel:
for name in assignment_frame.assigned_names:
self.writeline('context.vars[%r] = l_%s' % (name, name))
- self.writeline('context.exported_vars.add(%r)' % name)
+ if not name.startswith('__'):
+ self.writeline('context.exported_vars.add(%r)' % name)
def visit_Name(self, node, frame):
- if node.ctx == 'store':
- if frame.toplevel:
- frame.assigned_names.add(node.name)
- frame.name_overrides.pop(node.name, None)
- elif node.ctx == 'load':
- if node.name in frame.name_overrides:
- self.write(frame.name_overrides[node.name])
- return
+ if node.ctx == 'store' and frame.toplevel:
+ frame.assigned_names.add(node.name)
self.write('l_' + node.name)
def visit_Const(self, node, frame):