From ee832194cd9f55f75e5a51359b709d535efe957f Mon Sep 17 00:00:00 2001 From: Kevin Brown-Silva Date: Mon, 2 May 2022 12:01:08 -0600 Subject: [PATCH] Add support for namespaces in tuple assignment 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 | 2 ++ docs/templates.rst | 3 +++ src/jinja2/compiler.py | 23 ++++++++++++++++------- src/jinja2/parser.py | 30 +++++++++++++++++------------- tests/test_core_tags.py | 8 ++++++++ 5 files changed, 46 insertions(+), 20 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 521f5a08a..2b8179855 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 diff --git a/docs/templates.rst b/docs/templates.rst index 8db8ccaf9..9f376a13c 100644 --- a/docs/templates.rst +++ b/docs/templates.rst @@ -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 ---------- diff --git a/src/jinja2/compiler.py b/src/jinja2/compiler.py index ca079070a..0666cddf7 100644 --- a/src/jinja2/compiler.py +++ b/src/jinja2/compiler.py @@ -1581,6 +1581,22 @@ def visit_Output(self, node: nodes.Output, frame: Frame) -> None: 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 @@ def visit_NSRef(self, node: nodes.NSRef, frame: Frame) -> None: # `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: diff --git a/src/jinja2/parser.py b/src/jinja2/parser.py index 22f3f81f7..107232631 100644 --- a/src/jinja2/parser.py +++ b/src/jinja2/parser.py @@ -487,21 +487,18 @@ def parse_assign_target( """ 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 @@ def parse_unary(self, with_filter: bool = True) -> nodes.Expr: 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 @@ def parse_primary(self) -> nodes.Expr: 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 @@ def parse_tuple( 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 @@ def parse_tuple( """ 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 diff --git a/tests/test_core_tags.py b/tests/test_core_tags.py index 4bb95e024..2d847a2c9 100644 --- a/tests/test_core_tags.py +++ b/tests/test_core_tags.py @@ -538,6 +538,14 @@ def test_namespace_macro(self, env_trim): ) 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(