From: Adrian Moennich Date: Sun, 26 Feb 2017 15:45:54 +0000 (+0100) Subject: Add support for namespace attribute assignment X-Git-Tag: 2.10~24^2~3 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c2ee56db0e5e3a5a321285a85550958955b88d02;p=thirdparty%2Fjinja.git Add support for namespace attribute assignment --- diff --git a/CHANGES b/CHANGES index a684130b..81098b3e 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,9 @@ Version 2.10 - 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 ------------- diff --git a/jinja2/compiler.py b/jinja2/compiler.py index eb567c79..2cdec84b 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -1403,6 +1403,18 @@ class CodeGenerator(NodeVisitor): 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): diff --git a/jinja2/defaults.py b/jinja2/defaults.py index 35903883..6970e888 100644 --- a/jinja2/defaults.py +++ b/jinja2/defaults.py @@ -9,7 +9,7 @@ :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 @@ -35,7 +35,8 @@ DEFAULT_NAMESPACE = { 'dict': dict, 'lipsum': generate_lorem_ipsum, 'cycler': Cycler, - 'joiner': Joiner + 'joiner': Joiner, + 'namespace': Namespace } diff --git a/jinja2/idtracking.py b/jinja2/idtracking.py index db60c443..59a621d9 100644 --- a/jinja2/idtracking.py +++ b/jinja2/idtracking.py @@ -215,6 +215,9 @@ class FrameSymbolVisitor(NodeVisitor): 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) diff --git a/jinja2/nodes.py b/jinja2/nodes.py index 9903d43e..ab0e6b00 100644 --- a/jinja2/nodes.py +++ b/jinja2/nodes.py @@ -465,6 +465,18 @@ class Name(Expr): '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 diff --git a/jinja2/parser.py b/jinja2/parser.py index 0bf74c94..6d1fff6a 100644 --- a/jinja2/parser.py +++ b/jinja2/parser.py @@ -176,7 +176,7 @@ class Parser(object): 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) @@ -395,15 +395,21 @@ class Parser(object): 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: diff --git a/jinja2/runtime.py b/jinja2/runtime.py index a75ae6ef..5916d90b 100644 --- a/jinja2/runtime.py +++ b/jinja2/runtime.py @@ -15,7 +15,7 @@ from types import MethodType 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, \ @@ -27,7 +27,7 @@ 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. diff --git a/jinja2/utils.py b/jinja2/utils.py index b96d3095..3cece1a6 100644 --- a/jinja2/utils.py +++ b/jinja2/utils.py @@ -612,6 +612,18 @@ class Joiner(object): 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 '' % self.__dict__ + + # does this python version support async for in and async generators? try: exec('async def _():\n async for _ in ():\n yield _')