Skip to content

Commit fbc3a69

Browse files
authored
Add support for namespaces in tuple parsing (#1664)
2 parents 1d55cdd + b8f4831 commit fbc3a69

File tree

5 files changed

+64
-26
lines changed

5 files changed

+64
-26
lines changed

CHANGES.rst

Lines changed: 2 additions & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1581,6 +1581,29 @@ 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+
# ``a.b`` is allowed for assignment, and is parsed as an NSRef. However,
1586+
# it is only valid if it references a Namespace object. Emit a check for
1587+
# that for each ref here, before assignment code is emitted. This can't
1588+
# be done in visit_NSRef as the ref could be in the middle of a tuple.
1589+
seen_refs: t.Set[str] = set()
1590+
1591+
for nsref in node.find_all(nodes.NSRef):
1592+
if nsref.name in seen_refs:
1593+
# Only emit the check for each reference once, in case the same
1594+
# ref is used multiple times in a tuple, `ns.a, ns.b = c, d`.
1595+
continue
1596+
1597+
seen_refs.add(nsref.name)
1598+
ref = frame.symbols.ref(nsref.name)
1599+
self.writeline(f"if not isinstance({ref}, Namespace):")
1600+
self.indent()
1601+
self.writeline(
1602+
"raise TemplateRuntimeError"
1603+
'("cannot assign attribute on non-namespace object")'
1604+
)
1605+
self.outdent()
1606+
15841607
self.newline(node)
15851608
self.visit(node.target, frame)
15861609
self.write(" = ")
@@ -1637,17 +1660,11 @@ def visit_Name(self, node: nodes.Name, frame: Frame) -> None:
16371660
self.write(ref)
16381661

16391662
def visit_NSRef(self, node: nodes.NSRef, frame: Frame) -> None:
1640-
# NSRefs can only be used to store values; since they use the normal
1641-
# `foo.bar` notation they will be parsed as a normal attribute access
1642-
# when used anywhere but in a `set` context
1663+
# NSRef is a dotted assignment target a.b=c, but uses a[b]=c internally.
1664+
# visit_Assign emits code to validate that each ref is to a Namespace
1665+
# object only. That can't be emitted here as the ref could be in the
1666+
# middle of a tuple assignment.
16431667
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()
16511668
self.writeline(f"{ref}[{node.attr!r}]")
16521669

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

src/jinja2/parser.py

Lines changed: 24 additions & 16 deletions
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,17 +640,25 @@ 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:
644+
"""Parse a name or literal value. If ``with_namespace`` is enabled, also
645+
parse namespace attr refs, for use in assignments."""
647646
token = self.stream.current
648647
node: nodes.Expr
649648
if token.type == "name":
649+
next(self.stream)
650650
if token.value in ("true", "false", "True", "False"):
651651
node = nodes.Const(token.value in ("true", "True"), lineno=token.lineno)
652652
elif token.value in ("none", "None"):
653653
node = nodes.Const(None, lineno=token.lineno)
654+
elif with_namespace and self.stream.current.type == "dot":
655+
# If namespace attributes are allowed at this point, and the next
656+
# token is a dot, produce a namespace reference.
657+
next(self.stream)
658+
attr = self.stream.expect("name")
659+
node = nodes.NSRef(token.value, attr.value, lineno=token.lineno)
654660
else:
655661
node = nodes.Name(token.value, "load", lineno=token.lineno)
656-
next(self.stream)
657662
elif token.type == "string":
658663
next(self.stream)
659664
buf = [token.value]
@@ -683,15 +688,17 @@ def parse_tuple(
683688
with_condexpr: bool = True,
684689
extra_end_rules: t.Optional[t.Tuple[str, ...]] = None,
685690
explicit_parentheses: bool = False,
691+
with_namespace: bool = False,
686692
) -> t.Union[nodes.Tuple, nodes.Expr]:
687693
"""Works like `parse_expression` but if multiple expressions are
688694
delimited by a comma a :class:`~jinja2.nodes.Tuple` node is created.
689695
This method could also return a regular expression instead of a tuple
690696
if no commas where found.
691697
692698
The default parsing mode is a full tuple. If `simplified` is `True`
693-
only names and literals are parsed. The `no_condexpr` parameter is
694-
forwarded to :meth:`parse_expression`.
699+
only names and literals are parsed; ``with_namespace`` allows namespace
700+
attr refs as well. The `no_condexpr` parameter is forwarded to
701+
:meth:`parse_expression`.
695702
696703
Because tuples do not require delimiters and may end in a bogus comma
697704
an extra hint is needed that marks the end of a tuple. For example
@@ -704,13 +711,14 @@ def parse_tuple(
704711
"""
705712
lineno = self.stream.current.lineno
706713
if simplified:
707-
parse = self.parse_primary
708-
elif with_condexpr:
709-
parse = self.parse_expression
714+
715+
def parse() -> nodes.Expr:
716+
return self.parse_primary(with_namespace=with_namespace)
717+
710718
else:
711719

712720
def parse() -> nodes.Expr:
713-
return self.parse_expression(with_condexpr=False)
721+
return self.parse_expression(with_condexpr=with_condexpr)
714722

715723
args: t.List[nodes.Expr] = []
716724
is_tuple = False

tests/test_core_tags.py

Lines changed: 8 additions & 0 deletions
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)