]> git.ipfire.org Git - thirdparty/jinja.git/commitdiff
Add support for namespaces in tuple assignment
authorKevin Brown-Silva <kevin@kevin-brown.com>
Mon, 2 May 2022 18:01:08 +0000 (12:01 -0600)
committerDavid Lord <davidism@gmail.com>
Fri, 20 Dec 2024 22:09:40 +0000 (14:09 -0800)
This fixes a bug that existed because namespaces within `{% set %}`
were treated as a special case. This special case had the side-effect
of bypassing the code which allows for tuples to be assigned to.

The solution was to make tuple handling (and by extension, primary token
handling) aware of namespaces so that namespace tokens can be handled
appropriately. This is handled in a backwards-compatible way which
ensures that we do not try to parse namespace tokens when we otherwise
would be expecting to parse out name tokens with attributes.

Namespace instance checks are moved earlier, and deduplicated, so that
all checks are done before the assignment. Otherwise, the check could be
emitted in the middle of the tuple.

CHANGES.rst
docs/templates.rst
src/jinja2/compiler.py
src/jinja2/parser.py
tests/test_core_tags.py

index 521f5a08a3984b6e14b2c5d03f177f1d778bbf6a..2b81798555241beebe8e7cf94215798fd8c4b579 100644 (file)
@@ -43,6 +43,8 @@ Unreleased
 -   ``urlize`` does not add ``mailto:`` to values like `@a@b`. :pr:`1870`
 -   Tests decorated with `@pass_context`` can be used with the ``|select``
     filter. :issue:`1624`
+-   Using ``set`` for multiple assignment (``a, b = 1, 2``) does not fail when the
+    target is a namespace attribute. :issue:`1413`
 
 
 Version 3.1.4
index 8db8ccaf9ce3dab58dd1e7358ad109bafdc73270..9f376a13c10e4fb6ea6c5375eaea5eb60e5d5643 100644 (file)
@@ -1678,6 +1678,9 @@ The following functions are available in the global scope by default:
 
     .. versionadded:: 2.10
 
+    .. versionchanged:: 3.2
+        Namespace attributes can be assigned to in multiple assignment.
+
 
 Extensions
 ----------
index ca079070a177ea4f27e1e218524cc72a07ef4e1d..0666cddf77bdcecf0d1e001d461503e2577accf7 100644 (file)
@@ -1581,6 +1581,22 @@ class CodeGenerator(NodeVisitor):
 
     def visit_Assign(self, node: nodes.Assign, frame: Frame) -> None:
         self.push_assign_tracking()
+
+        # NSRef can only ever be used during assignment so we need to check
+        # to make sure that it is only being used to assign using a Namespace.
+        # This check is done here because it is used an expression during the
+        # assignment and therefore cannot have this check done when the NSRef
+        # node is visited
+        for nsref in node.find_all(nodes.NSRef):
+            ref = frame.symbols.ref(nsref.name)
+            self.writeline(f"if not isinstance({ref}, Namespace):")
+            self.indent()
+            self.writeline(
+                "raise TemplateRuntimeError"
+                '("cannot assign attribute on non-namespace object")'
+            )
+            self.outdent()
+
         self.newline(node)
         self.visit(node.target, frame)
         self.write(" = ")
@@ -1641,13 +1657,6 @@ class CodeGenerator(NodeVisitor):
         # `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(f"if not isinstance({ref}, Namespace):")
-        self.indent()
-        self.writeline(
-            "raise TemplateRuntimeError"
-            '("cannot assign attribute on non-namespace object")'
-        )
-        self.outdent()
         self.writeline(f"{ref}[{node.attr!r}]")
 
     def visit_Const(self, node: nodes.Const, frame: Frame) -> None:
index 22f3f81f7ec4f77e15a737a7d680d06a296fc04c..10723263188112762305eb926b70325b71d164d7 100644 (file)
@@ -487,21 +487,18 @@ class Parser:
         """
         target: nodes.Expr
 
-        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:
+        if name_only:
             token = self.stream.expect("name")
             target = nodes.Name(token.value, "store", lineno=token.lineno)
         else:
             if with_tuple:
                 target = self.parse_tuple(
-                    simplified=True, extra_end_rules=extra_end_rules
+                    simplified=True,
+                    extra_end_rules=extra_end_rules,
+                    with_namespace=with_namespace,
                 )
             else:
-                target = self.parse_primary()
+                target = self.parse_primary(with_namespace=with_namespace)
 
             target.set_ctx("store")
 
@@ -643,7 +640,7 @@ class Parser:
             node = self.parse_filter_expr(node)
         return node
 
-    def parse_primary(self) -> nodes.Expr:
+    def parse_primary(self, with_namespace: bool = False) -> nodes.Expr:
         token = self.stream.current
         node: nodes.Expr
         if token.type == "name":
@@ -651,6 +648,11 @@ class Parser:
                 node = nodes.Const(token.value in ("true", "True"), lineno=token.lineno)
             elif token.value in ("none", "None"):
                 node = nodes.Const(None, lineno=token.lineno)
+            elif with_namespace and self.stream.look().type == "dot":
+                next(self.stream)  # token
+                next(self.stream)  # dot
+                attr = self.stream.current
+                node = nodes.NSRef(token.value, attr.value, lineno=token.lineno)
             else:
                 node = nodes.Name(token.value, "load", lineno=token.lineno)
             next(self.stream)
@@ -683,6 +685,7 @@ class Parser:
         with_condexpr: bool = True,
         extra_end_rules: t.Optional[t.Tuple[str, ...]] = None,
         explicit_parentheses: bool = False,
+        with_namespace: bool = False,
     ) -> t.Union[nodes.Tuple, nodes.Expr]:
         """Works like `parse_expression` but if multiple expressions are
         delimited by a comma a :class:`~jinja2.nodes.Tuple` node is created.
@@ -704,13 +707,14 @@ class Parser:
         """
         lineno = self.stream.current.lineno
         if simplified:
-            parse = self.parse_primary
-        elif with_condexpr:
-            parse = self.parse_expression
+
+            def parse() -> nodes.Expr:
+                return self.parse_primary(with_namespace=with_namespace)
+
         else:
 
             def parse() -> nodes.Expr:
-                return self.parse_expression(with_condexpr=False)
+                return self.parse_expression(with_condexpr=with_condexpr)
 
         args: t.List[nodes.Expr] = []
         is_tuple = False
index 4bb95e0240a021845ec3d9431f251bd023c0a1af..2d847a2c9afac0e30115dc5d401c700867d029ad 100644 (file)
@@ -538,6 +538,14 @@ class TestSet:
         )
         assert tmpl.render() == "13|37"
 
+    def test_namespace_set_tuple(self, env_trim):
+        tmpl = env_trim.from_string(
+            "{% set ns = namespace(a=12, b=36) %}"
+            "{% set ns.a, ns.b = ns.a + 1, ns.b + 1 %}"
+            "{{ ns.a }}|{{ ns.b }}"
+        )
+        assert tmpl.render() == "13|37"
+
     def test_block_escaping_filtered(self):
         env = Environment(autoescape=True)
         tmpl = env.from_string(