]> git.ipfire.org Git - thirdparty/jinja.git/commitdiff
more comments about nsref assignment 1664/head
authorDavid Lord <davidism@gmail.com>
Fri, 20 Dec 2024 22:02:31 +0000 (14:02 -0800)
committerDavid Lord <davidism@gmail.com>
Fri, 20 Dec 2024 22:49:58 +0000 (14:49 -0800)
only emit nsref instance check once per ref name
refactor primary name parsing a bit

src/jinja2/compiler.py
src/jinja2/parser.py

index 0666cddf77bdcecf0d1e001d461503e2577accf7..a4ff6a1b11af3e1a868d1a74c48d842390259b43 100644 (file)
@@ -1582,12 +1582,19 @@ 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
+        # ``a.b`` is allowed for assignment, and is parsed as an NSRef. However,
+        # it is only valid if it references a Namespace object. Emit a check for
+        # that for each ref here, before assignment code is emitted. This can't
+        # be done in visit_NSRef as the ref could be in the middle of a tuple.
+        seen_refs: t.Set[str] = set()
+
         for nsref in node.find_all(nodes.NSRef):
+            if nsref.name in seen_refs:
+                # Only emit the check for each reference once, in case the same
+                # ref is used multiple times in a tuple, `ns.a, ns.b = c, d`.
+                continue
+
+            seen_refs.add(nsref.name)
             ref = frame.symbols.ref(nsref.name)
             self.writeline(f"if not isinstance({ref}, Namespace):")
             self.indent()
@@ -1653,9 +1660,10 @@ class CodeGenerator(NodeVisitor):
         self.write(ref)
 
     def visit_NSRef(self, node: nodes.NSRef, frame: Frame) -> None:
-        # 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
+        # NSRef is a dotted assignment target a.b=c, but uses a[b]=c internally.
+        # visit_Assign emits code to validate that each ref is to a Namespace
+        # object only. That can't be emitted here as the ref could be in the
+        # middle of a tuple assignment.
         ref = frame.symbols.ref(node.name)
         self.writeline(f"{ref}[{node.attr!r}]")
 
index 10723263188112762305eb926b70325b71d164d7..f4117754aaf94ae525d183fb3c36fae1dc8afd62 100644 (file)
@@ -641,21 +641,24 @@ class Parser:
         return node
 
     def parse_primary(self, with_namespace: bool = False) -> nodes.Expr:
+        """Parse a name or literal value. If ``with_namespace`` is enabled, also
+        parse namespace attr refs, for use in assignments."""
         token = self.stream.current
         node: nodes.Expr
         if token.type == "name":
+            next(self.stream)
             if token.value in ("true", "false", "True", "False"):
                 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
+            elif with_namespace and self.stream.current.type == "dot":
+                # If namespace attributes are allowed at this point, and the next
+                # token is a dot, produce a namespace reference.
+                next(self.stream)
+                attr = self.stream.expect("name")
                 node = nodes.NSRef(token.value, attr.value, lineno=token.lineno)
             else:
                 node = nodes.Name(token.value, "load", lineno=token.lineno)
-            next(self.stream)
         elif token.type == "string":
             next(self.stream)
             buf = [token.value]
@@ -693,8 +696,9 @@ class Parser:
         if no commas where found.
 
         The default parsing mode is a full tuple.  If `simplified` is `True`
-        only names and literals are parsed.  The `no_condexpr` parameter is
-        forwarded to :meth:`parse_expression`.
+        only names and literals are parsed; ``with_namespace`` allows namespace
+        attr refs as well. The `no_condexpr` parameter is forwarded to
+        :meth:`parse_expression`.
 
         Because tuples do not require delimiters and may end in a bogus comma
         an extra hint is needed that marks the end of a tuple.  For example