Skip to content

Commit

Permalink
Add support for namespaces in tuple assignment
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
kevin-brown authored and davidism committed Dec 20, 2024
1 parent 1d55cdd commit ee83219
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 20 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/templates.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------
Expand Down
23 changes: 16 additions & 7 deletions src/jinja2/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(" = ")
Expand Down Expand Up @@ -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:
Expand Down
30 changes: 17 additions & 13 deletions src/jinja2/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -643,14 +640,19 @@ 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":
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
node = nodes.NSRef(token.value, attr.value, lineno=token.lineno)
else:
node = nodes.Name(token.value, "load", lineno=token.lineno)
next(self.stream)
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions tests/test_core_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit ee83219

Please sign in to comment.