- Added `changed(*values)` to loop contexts, providing an easy way of checking
whether a value has changed since the last iteration (or rather since the
last call of the method)
+- Added a `namespace` function that creates a special object which allows
+ attribute assignment using the `set` tag. This can be used to carry data
+ across scopes, e.g. from a loop body to code that comes after the loop.
Version 2.9.6
-------------
self.write(ref)
+ def visit_NSRef(self, node, frame):
+ # NSRefs can only be used to store values; since they use the normal
+ # `foo.bar` notation they will be parsed as a normal attribute access
+ # when used anywhere but in a `set` context
+ ref = frame.symbols.ref(node.name)
+ self.writeline('if not isinstance(%s, Namespace):' % ref)
+ self.indent()
+ self.writeline('raise TemplateRuntimeError(%r)' %
+ 'cannot assign attribute on non-namespace object')
+ self.outdent()
+ self.writeline('%s.%s' % (ref, node.attr))
+
def visit_Const(self, node, frame):
val = node.as_const(frame.eval_ctx)
if isinstance(val, float):
:license: BSD, see LICENSE for more details.
"""
from jinja2._compat import range_type
-from jinja2.utils import generate_lorem_ipsum, Cycler, Joiner
+from jinja2.utils import generate_lorem_ipsum, Cycler, Joiner, Namespace
# defaults for the parser / lexer
'dict': dict,
'lipsum': generate_lorem_ipsum,
'cycler': Cycler,
- 'joiner': Joiner
+ 'joiner': Joiner,
+ 'namespace': Namespace
}
elif node.ctx == 'load':
self.symbols.load(node.name)
+ def visit_NSRef(self, node, **kwargs):
+ self.symbols.load(node.name)
+
def visit_If(self, node, **kwargs):
self.visit(node.test, **kwargs)
'True', 'False', 'None')
+class NSRef(Expr):
+ """Reference to a namespace value assignment"""
+ fields = ('name', 'attr')
+
+ def can_assign(self):
+ # We don't need any special checks here; NSRef assignments have a
+ # runtime check to ensure the target is a namespace object which will
+ # have been checked already as it is created using a normal assignment
+ # which goes through a `Name` node.
+ return True
+
+
class Literal(Expr):
"""Baseclass for literals."""
abstract = True
def parse_set(self):
"""Parse an assign statement."""
lineno = next(self.stream).lineno
- target = self.parse_assign_target()
+ target = self.parse_assign_target(with_namespace=True)
if self.stream.skip_if('assign'):
expr = self.parse_tuple()
return nodes.Assign(target, expr, lineno=lineno)
return node
def parse_assign_target(self, with_tuple=True, name_only=False,
- extra_end_rules=None):
+ extra_end_rules=None, with_namespace=False):
"""Parse an assignment target. As Jinja2 allows assignments to
tuples, this function can parse all allowed assignment targets. Per
default assignments to tuples are parsed, that can be disable however
by setting `with_tuple` to `False`. If only assignments to names are
wanted `name_only` can be set to `True`. The `extra_end_rules`
- parameter is forwarded to the tuple parsing function.
+ parameter is forwarded to the tuple parsing function. If
+ `with_namespace` is enabled, a namespace assignment may be parsed.
"""
- if name_only:
+ if with_namespace and self.stream.look().type == 'dot':
+ token = self.stream.expect('name')
+ next(self.stream) # dot
+ attr = self.stream.expect('name')
+ target = nodes.NSRef(token.value, attr.value, lineno=token.lineno)
+ elif name_only:
token = self.stream.expect('name')
target = nodes.Name(token.value, 'store', lineno=token.lineno)
else:
from jinja2.nodes import EvalContext, _context_function_types
from jinja2.utils import Markup, soft_unicode, escape, missing, concat, \
- internalcode, object_type_repr, evalcontextfunction
+ internalcode, object_type_repr, evalcontextfunction, Namespace
from jinja2.exceptions import UndefinedError, TemplateRuntimeError, \
TemplateNotFound
from jinja2._compat import imap, text_type, iteritems, \
__all__ = ['LoopContext', 'TemplateReference', 'Macro', 'Markup',
'TemplateRuntimeError', 'missing', 'concat', 'escape',
'markup_join', 'unicode_join', 'to_string', 'identity',
- 'TemplateNotFound']
+ 'TemplateNotFound', 'Namespace']
#: the name of the function that is used to convert something into
#: a string. We can just use the text type here.
return self.sep
+class Namespace(object):
+ """A namespace object that can hold arbitrary attributes. It may be
+ initialized from a dictionary or with keyword argments."""
+
+ def __init__(*args, **kwargs):
+ self, args = args[0], args[1:]
+ self.__dict__.update(dict(*args, **kwargs))
+
+ def __repr__(self):
+ return '<Namespace %r>' % self.__dict__
+
+
# does this python version support async for in and async generators?
try:
exec('async def _():\n async for _ in ():\n yield _')