Skip to content

Commit bc0a686

Browse files
sobolevnpablogsalhauntsaninja
authored
gh-87447: Fix walrus comprehension rebind checking (#100581)
Co-authored-by: Pablo Galindo Salgado <[email protected]> Co-authored-by: Shantanu <[email protected]>
1 parent 8d69828 commit bc0a686

File tree

4 files changed

+92
-3
lines changed

4 files changed

+92
-3
lines changed

Doc/whatsnew/3.12.rst

+7
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,13 @@ Other Language Changes
182182
arguments of any type instead of just :class:`bool` and :class:`int`.
183183
(Contributed by Serhiy Storchaka in :gh:`60203`.)
184184

185+
* Variables used in the target part of comprehensions that are not stored to
186+
can now be used in assignment expressions (``:=``).
187+
For example, in ``[(b := 1) for a, b.prop in some_iter]``, the assignment to
188+
``b`` is now allowed. Note that assigning to variables stored to in the target
189+
part of comprehensions (like ``a``) is still disallowed, as per :pep:`572`.
190+
(Contributed by Nikita Sobolev in :gh:`100581`.)
191+
185192

186193
New Modules
187194
===========

Lib/test/test_named_expressions.py

+78-2
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,69 @@ def test_named_expression_invalid_in_class_body(self):
114114
"assignment expression within a comprehension cannot be used in a class body"):
115115
exec(code, {}, {})
116116

117+
def test_named_expression_valid_rebinding_iteration_variable(self):
118+
# This test covers that we can reassign variables
119+
# that are not directly assigned in the
120+
# iterable part of a comprehension.
121+
cases = [
122+
# Regression tests from https://github.com/python/cpython/issues/87447
123+
("Complex expression: c",
124+
"{0}(c := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
125+
("Complex expression: d",
126+
"{0}(d := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
127+
("Complex expression: e",
128+
"{0}(e := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
129+
("Complex expression: f",
130+
"{0}(f := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
131+
("Complex expression: g",
132+
"{0}(g := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
133+
("Complex expression: h",
134+
"{0}(h := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
135+
("Complex expression: i",
136+
"{0}(i := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
137+
("Complex expression: j",
138+
"{0}(j := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
139+
]
140+
for test_case, code in cases:
141+
for lpar, rpar in [('(', ')'), ('[', ']'), ('{', '}')]:
142+
code = code.format(lpar, rpar)
143+
with self.subTest(case=test_case, lpar=lpar, rpar=rpar):
144+
# Names used in snippets are not defined,
145+
# but we are fine with it: just must not be a SyntaxError.
146+
# Names used in snippets are not defined,
147+
# but we are fine with it: just must not be a SyntaxError.
148+
with self.assertRaises(NameError):
149+
exec(code, {}) # Module scope
150+
with self.assertRaises(NameError):
151+
exec(code, {}, {}) # Class scope
152+
exec(f"lambda: {code}", {}) # Function scope
153+
154+
def test_named_expression_invalid_rebinding_iteration_variable(self):
155+
# This test covers that we cannot reassign variables
156+
# that are directly assigned in the iterable part of a comprehension.
157+
cases = [
158+
# Regression tests from https://github.com/python/cpython/issues/87447
159+
("Complex expression: a", "a",
160+
"{0}(a := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
161+
("Complex expression: b", "b",
162+
"{0}(b := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
163+
]
164+
for test_case, target, code in cases:
165+
msg = f"assignment expression cannot rebind comprehension iteration variable '{target}'"
166+
for lpar, rpar in [('(', ')'), ('[', ']'), ('{', '}')]:
167+
code = code.format(lpar, rpar)
168+
with self.subTest(case=test_case, lpar=lpar, rpar=rpar):
169+
# Names used in snippets are not defined,
170+
# but we are fine with it: just must not be a SyntaxError.
171+
# Names used in snippets are not defined,
172+
# but we are fine with it: just must not be a SyntaxError.
173+
with self.assertRaisesRegex(SyntaxError, msg):
174+
exec(code, {}) # Module scope
175+
with self.assertRaisesRegex(SyntaxError, msg):
176+
exec(code, {}, {}) # Class scope
177+
with self.assertRaisesRegex(SyntaxError, msg):
178+
exec(f"lambda: {code}", {}) # Function scope
179+
117180
def test_named_expression_invalid_rebinding_list_comprehension_iteration_variable(self):
118181
cases = [
119182
("Local reuse", 'i', "[i := 0 for i in range(5)]"),
@@ -129,7 +192,11 @@ def test_named_expression_invalid_rebinding_list_comprehension_iteration_variabl
129192
msg = f"assignment expression cannot rebind comprehension iteration variable '{target}'"
130193
with self.subTest(case=case):
131194
with self.assertRaisesRegex(SyntaxError, msg):
132-
exec(code, {}, {})
195+
exec(code, {}) # Module scope
196+
with self.assertRaisesRegex(SyntaxError, msg):
197+
exec(code, {}, {}) # Class scope
198+
with self.assertRaisesRegex(SyntaxError, msg):
199+
exec(f"lambda: {code}", {}) # Function scope
133200

134201
def test_named_expression_invalid_rebinding_list_comprehension_inner_loop(self):
135202
cases = [
@@ -178,12 +245,21 @@ def test_named_expression_invalid_rebinding_set_comprehension_iteration_variable
178245
("Unreachable reuse", 'i', "{False or (i:=0) for i in range(5)}"),
179246
("Unreachable nested reuse", 'i',
180247
"{(i, j) for i in range(5) for j in range(5) if True or (i:=10)}"),
248+
# Regression tests from https://github.com/python/cpython/issues/87447
249+
("Complex expression: a", "a",
250+
"{(a := 1) for a, (*b, c[d+e::f(g)], h.i) in j}"),
251+
("Complex expression: b", "b",
252+
"{(b := 1) for a, (*b, c[d+e::f(g)], h.i) in j}"),
181253
]
182254
for case, target, code in cases:
183255
msg = f"assignment expression cannot rebind comprehension iteration variable '{target}'"
184256
with self.subTest(case=case):
185257
with self.assertRaisesRegex(SyntaxError, msg):
186-
exec(code, {}, {})
258+
exec(code, {}) # Module scope
259+
with self.assertRaisesRegex(SyntaxError, msg):
260+
exec(code, {}, {}) # Class scope
261+
with self.assertRaisesRegex(SyntaxError, msg):
262+
exec(f"lambda: {code}", {}) # Function scope
187263

188264
def test_named_expression_invalid_rebinding_set_comprehension_inner_loop(self):
189265
cases = [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix :exc:`SyntaxError` on comprehension rebind checking with names that are
2+
not actually redefined.
3+
4+
Now reassigning ``b`` in ``[(b := 1) for a, b.prop in some_iter]`` is allowed.
5+
Reassigning ``a`` is still disallowed as per :pep:`572`.

Python/symtable.c

+2-1
Original file line numberDiff line numberDiff line change
@@ -1488,7 +1488,8 @@ symtable_extend_namedexpr_scope(struct symtable *st, expr_ty e)
14881488
*/
14891489
if (ste->ste_comprehension) {
14901490
long target_in_scope = _PyST_GetSymbol(ste, target_name);
1491-
if (target_in_scope & DEF_COMP_ITER) {
1491+
if ((target_in_scope & DEF_COMP_ITER) &&
1492+
(target_in_scope & DEF_LOCAL)) {
14921493
PyErr_Format(PyExc_SyntaxError, NAMED_EXPR_COMP_CONFLICT, target_name);
14931494
PyErr_RangedSyntaxLocationObject(st->st_filename,
14941495
e->lineno,

0 commit comments

Comments
 (0)