Skip to content

Commit ee83219

Browse files
kevin-browndavidism
authored andcommitted
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.
1 parent 1d55cdd commit ee83219

File tree

5 files changed

+46
-20
lines changed

5 files changed

+46
-20
lines changed

CHANGES.rst

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ Unreleased
4343
- ``urlize`` does not add ``mailto:`` to values like `@a@b`. :pr:`1870`
4444
- Tests decorated with `@pass_context`` can be used with the ``|select``
4545
filter. :issue:`1624`
46+
- Using ``set`` for multiple assignment (``a, b = 1, 2``) does not fail when the
47+
target is a namespace attribute. :issue:`1413`
4648

4749

4850
Version 3.1.4

docs/templates.rst

+3
Original file line numberDiff line numberDiff line change
@@ -1678,6 +1678,9 @@ The following functions are available in the global scope by default:
16781678

16791679
.. versionadded:: 2.10
16801680

1681+
.. versionchanged:: 3.2
1682+
Namespace attributes can be assigned to in multiple assignment.
1683+
16811684

16821685
Extensions
16831686
----------

src/jinja2/compiler.py

+16-7
Original file line numberDiff line numberDiff line change
@@ -1581,6 +1581,22 @@ def visit_Output(self, node: nodes.Output, frame: Frame) -> None:
15811581

15821582
def visit_Assign(self, node: nodes.Assign, frame: Frame) -> None:
15831583
self.push_assign_tracking()
1584+
1585+
# NSRef can only ever be used during assignment so we need to check
1586+
# to make sure that it is only being used to assign using a Namespace.
1587+
# This check is done here because it is used an expression during the
1588+
# assignment and therefore cannot have this check done when the NSRef
1589+
# node is visited
1590+
for nsref in node.find_all(nodes.NSRef):
1591+
ref = frame.symbols.ref(nsref.name)
1592+
self.writeline(f"if not isinstance({ref}, Namespace):")
1593+
self.indent()
1594+
self.writeline(
1595+
"raise TemplateRuntimeError"
1596+
'("cannot assign attribute on non-namespace object")'
1597+
)
1598+
self.outdent()
1599+
15841600
self.newline(node)
15851601
self.visit(node.target, frame)
15861602
self.write(" = ")
@@ -1641,13 +1657,6 @@ def visit_NSRef(self, node: nodes.NSRef, frame: Frame) -> None:
16411657
# `foo.bar` notation they will be parsed as a normal attribute access
16421658
# when used anywhere but in a `set` context
16431659
ref = frame.symbols.ref(node.name)
1644-
self.writeline(f"if not isinstance({ref}, Namespace):")
1645-
self.indent()
1646-
self.writeline(
1647-
"raise TemplateRuntimeError"
1648-
'("cannot assign attribute on non-namespace object")'
1649-
)
1650-
self.outdent()
16511660
self.writeline(f"{ref}[{node.attr!r}]")
16521661

16531662
def visit_Const(self, node: nodes.Const, frame: Frame) -> None:

src/jinja2/parser.py

+17-13
Original file line numberDiff line numberDiff line change
@@ -487,21 +487,18 @@ def parse_assign_target(
487487
"""
488488
target: nodes.Expr
489489

490-
if with_namespace and self.stream.look().type == "dot":
491-
token = self.stream.expect("name")
492-
next(self.stream) # dot
493-
attr = self.stream.expect("name")
494-
target = nodes.NSRef(token.value, attr.value, lineno=token.lineno)
495-
elif name_only:
490+
if name_only:
496491
token = self.stream.expect("name")
497492
target = nodes.Name(token.value, "store", lineno=token.lineno)
498493
else:
499494
if with_tuple:
500495
target = self.parse_tuple(
501-
simplified=True, extra_end_rules=extra_end_rules
496+
simplified=True,
497+
extra_end_rules=extra_end_rules,
498+
with_namespace=with_namespace,
502499
)
503500
else:
504-
target = self.parse_primary()
501+
target = self.parse_primary(with_namespace=with_namespace)
505502

506503
target.set_ctx("store")
507504

@@ -643,14 +640,19 @@ def parse_unary(self, with_filter: bool = True) -> nodes.Expr:
643640
node = self.parse_filter_expr(node)
644641
return node
645642

646-
def parse_primary(self) -> nodes.Expr:
643+
def parse_primary(self, with_namespace: bool = False) -> nodes.Expr:
647644
token = self.stream.current
648645
node: nodes.Expr
649646
if token.type == "name":
650647
if token.value in ("true", "false", "True", "False"):
651648
node = nodes.Const(token.value in ("true", "True"), lineno=token.lineno)
652649
elif token.value in ("none", "None"):
653650
node = nodes.Const(None, lineno=token.lineno)
651+
elif with_namespace and self.stream.look().type == "dot":
652+
next(self.stream) # token
653+
next(self.stream) # dot
654+
attr = self.stream.current
655+
node = nodes.NSRef(token.value, attr.value, lineno=token.lineno)
654656
else:
655657
node = nodes.Name(token.value, "load", lineno=token.lineno)
656658
next(self.stream)
@@ -683,6 +685,7 @@ def parse_tuple(
683685
with_condexpr: bool = True,
684686
extra_end_rules: t.Optional[t.Tuple[str, ...]] = None,
685687
explicit_parentheses: bool = False,
688+
with_namespace: bool = False,
686689
) -> t.Union[nodes.Tuple, nodes.Expr]:
687690
"""Works like `parse_expression` but if multiple expressions are
688691
delimited by a comma a :class:`~jinja2.nodes.Tuple` node is created.
@@ -704,13 +707,14 @@ def parse_tuple(
704707
"""
705708
lineno = self.stream.current.lineno
706709
if simplified:
707-
parse = self.parse_primary
708-
elif with_condexpr:
709-
parse = self.parse_expression
710+
711+
def parse() -> nodes.Expr:
712+
return self.parse_primary(with_namespace=with_namespace)
713+
710714
else:
711715

712716
def parse() -> nodes.Expr:
713-
return self.parse_expression(with_condexpr=False)
717+
return self.parse_expression(with_condexpr=with_condexpr)
714718

715719
args: t.List[nodes.Expr] = []
716720
is_tuple = False

tests/test_core_tags.py

+8
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,14 @@ def test_namespace_macro(self, env_trim):
538538
)
539539
assert tmpl.render() == "13|37"
540540

541+
def test_namespace_set_tuple(self, env_trim):
542+
tmpl = env_trim.from_string(
543+
"{% set ns = namespace(a=12, b=36) %}"
544+
"{% set ns.a, ns.b = ns.a + 1, ns.b + 1 %}"
545+
"{{ ns.a }}|{{ ns.b }}"
546+
)
547+
assert tmpl.render() == "13|37"
548+
541549
def test_block_escaping_filtered(self):
542550
env = Environment(autoescape=True)
543551
tmpl = env.from_string(

0 commit comments

Comments
 (0)