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(