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.
This commit is contained in:
parent
1d55cddbb2
commit
ee832194cd
@ -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
|
||||
|
||||
@ -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
|
||||
----------
|
||||
|
||||
@ -1581,6 +1581,22 @@ class CodeGenerator(NodeVisitor):
|
||||
|
||||
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 @@ class CodeGenerator(NodeVisitor):
|
||||
# `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:
|
||||
|
||||
@ -487,21 +487,18 @@ class Parser:
|
||||
"""
|
||||
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 @@ class Parser:
|
||||
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 @@ class Parser:
|
||||
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 @@ class Parser:
|
||||
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 @@ class Parser:
|
||||
"""
|
||||
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
|
||||
|
||||
@ -538,6 +538,14 @@ class TestSet:
|
||||
)
|
||||
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(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user