from copy import deepcopy
from jinja2 import nodes
from jinja2.visitor import NodeVisitor, NodeTransformer
-from jinja2.runtime import subscribe
+from jinja2.runtime import subscribe, LoopContext
class ContextStack(object):
def pop(self):
self.stack.pop()
+ def get(self, key, default=None):
+ try:
+ return self[key]
+ except KeyError:
+ return default
+
def __getitem__(self, key):
for level in reversed(self.stack):
if key in level:
def __init__(self, environment):
self.environment = environment
+ def visit_Block(self, node, context):
+ return self.generic_visit(node, context.blank())
+
def visit_Filter(self, node, context):
"""Try to evaluate filters if possible."""
# XXX: nonconstant arguments? not-called visitors? generic visit!
iterable = iter(self.visit(node.iter, context).as_const())
except (nodes.Impossible, TypeError):
return self.generic_visit(node, context)
+
+ parent = context.get('loop')
context.push()
result = []
- # XXX: tuple unpacking (for key, value in foo)
- target = node.target.name
iterated = False
- for item in iterable:
- context[target] = item
- result.extend(self.visit(n, context) for n in deepcopy(node.body))
- iterated = True
- if not iterated and node.else_:
- result.extend(self.visit(n, context) for n in deepcopy(node.else_))
- context.pop()
+
+ def assign(target, value):
+ if isinstance(target, nodes.Name):
+ context[target.name] = value
+ elif isinstance(target, nodes.Tuple):
+ try:
+ value = tuple(value)
+ except TypeError:
+ raise nodes.Impossible()
+ if len(target.items) != len(value):
+ raise nodes.Impossible()
+ for name, val in zip(target.items, value):
+ assign(name, val)
+ else:
+ raise AssertionError('unexpected assignable node')
+
+ # XXX: not covered cases:
+ # - item is accessed by dynamic part in the iteration
+ try:
+ try:
+ for loop, item in LoopContext(iterable, parent):
+ context['loop'] = loop
+ assign(node.target, item)
+ result.extend(self.visit(n, context)
+ for n in deepcopy(node.body))
+ iterated = True
+ if not iterated and node.else_:
+ result.extend(self.visit(n, context)
+ for n in deepcopy(node.else_))
+ except nodes.Impossible:
+ return node
+ finally:
+ context.pop()
return result
def visit_If(self, node, context):
value = tuple(value)
except TypeError:
raise nodes.Impossible()
- if len(target) != len(value):
+ if len(target.items) != len(value):
raise nodes.Impossible()
- for name, val in zip(target, value):
+ for name, val in zip(target.items, value):
walk(name, val)
else:
raise AssertionError('unexpected assignable node')
return node
return result
+ def fold(self, node, context):
+ """Do constant folding."""
+ node = self.generic_visit(node, context)
+ try:
+ return nodes.Const(node.as_const(), lineno=node.lineno)
+ except nodes.Impossible:
+ return node
+ visit_Add = visit_Sub = visit_Mul = visit_Div = visit_FloorDiv = \
+ visit_Pow = visit_Mod = visit_And = visit_Or = visit_Pos = visit_Neg = \
+ visit_Not = visit_Compare = fold
+
def visit_Subscript(self, node, context):
if node.ctx == 'load':
try:
defaultdict = None
-__all__ = ['extends', 'subscribe', 'TemplateContext', 'Macro']
+__all__ = ['extends', 'subscribe', 'LoopContext', 'TemplateContext', 'Macro']
def extends(template, namespace):
return self.undefined_factory(key)
+class LoopContext(object):
+ """Helper for extended iteration."""
+
+ def __init__(self, iterable, parent=None):
+ self._iterable = iterable
+ self.index0 = 0
+ self.parent = parent
+
+ def __iter__(self):
+ for item in self._iterable:
+ yield self, item
+ self.index0 += 1
+
+ first = property(lambda x: x.index0 == 0)
+ last = property(lambda x: x.revindex0 == 0)
+ index = property(lambda x: x.index0 + 1)
+ revindex = property(lambda x: x.length)
+ revindex0 = property(lambda x: x.length - 1)
+
+ @property
+ def length(self):
+ if not hasattr(self, '_length'):
+ try:
+ length = len(self._iterable)
+ except TypeError:
+ length = len(tuple(self._iterable))
+ self._length = length
+ return self._length
+
+
class Macro(object):
"""
Wraps a macor