]> git.ipfire.org Git - thirdparty/jinja.git/commitdiff
Add support for namespace attribute assignment
authorAdrian Moennich <adrian@planetcoding.net>
Sun, 26 Feb 2017 15:45:54 +0000 (16:45 +0100)
committerAdrian Moennich <adrian@planetcoding.net>
Sat, 24 Jun 2017 08:56:54 +0000 (10:56 +0200)
CHANGES
jinja2/compiler.py
jinja2/defaults.py
jinja2/idtracking.py
jinja2/nodes.py
jinja2/parser.py
jinja2/runtime.py
jinja2/utils.py

diff --git a/CHANGES b/CHANGES
index a684130b8bb5438732999d76d021d734ffe09907..81098b3e7c61f37dcff7bdd97e26f794094d493e 100644 (file)
--- 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
 -------------
index eb567c798a152183121c6ca6dfe62002642d7047..2cdec84bd46b5055a44fac0463389ebf16b5fa5e 100644 (file)
@@ -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):
index 35903883cd3ccda829d62fc61424521a7411ec72..6970e88815f35163e3a065e958dc240faa251087 100644 (file)
@@ -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
 }
 
 
index db60c443a74308091ff6b2dfa96dc64535a6a9a9..59a621d9d05bc7ee6b2479b4ab4210ee1e9cb990 100644 (file)
@@ -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)
 
index 9903d43e1fed99415313abe5eaa84dd10443c8c2..ab0e6b00c44e809f2d7b41ace4b81fa07f2a5cc9 100644 (file)
@@ -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
index 0bf74c9459be00c325b12be338f4925bb6bfcfe2..6d1fff6a9955ae4e2940b00e0adc8b6a357af6b8 100644 (file)
@@ -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:
index a75ae6ef15c860adf643768756b0fd12c791d9ce..5916d90bd2fcb1a435fe3ca64366d430399d8f40 100644 (file)
@@ -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.
index b96d30954607ae6333fe2c935949537deefc10d7..3cece1a6b2d0bc00be15f63f13f51ef27ac16e25 100644 (file)
@@ -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 '<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 _')