mirror of
https://github.com/python/cpython.git
synced 2025-09-27 02:39:58 +00:00
gh-87447: Fix walrus comprehension rebind checking (#100581)
Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com>
This commit is contained in:
parent
8d69828092
commit
bc0a686f82
4 changed files with 92 additions and 3 deletions
|
@ -182,6 +182,13 @@ Other Language Changes
|
||||||
arguments of any type instead of just :class:`bool` and :class:`int`.
|
arguments of any type instead of just :class:`bool` and :class:`int`.
|
||||||
(Contributed by Serhiy Storchaka in :gh:`60203`.)
|
(Contributed by Serhiy Storchaka in :gh:`60203`.)
|
||||||
|
|
||||||
|
* Variables used in the target part of comprehensions that are not stored to
|
||||||
|
can now be used in assignment expressions (``:=``).
|
||||||
|
For example, in ``[(b := 1) for a, b.prop in some_iter]``, the assignment to
|
||||||
|
``b`` is now allowed. Note that assigning to variables stored to in the target
|
||||||
|
part of comprehensions (like ``a``) is still disallowed, as per :pep:`572`.
|
||||||
|
(Contributed by Nikita Sobolev in :gh:`100581`.)
|
||||||
|
|
||||||
|
|
||||||
New Modules
|
New Modules
|
||||||
===========
|
===========
|
||||||
|
|
|
@ -114,6 +114,69 @@ class NamedExpressionInvalidTest(unittest.TestCase):
|
||||||
"assignment expression within a comprehension cannot be used in a class body"):
|
"assignment expression within a comprehension cannot be used in a class body"):
|
||||||
exec(code, {}, {})
|
exec(code, {}, {})
|
||||||
|
|
||||||
|
def test_named_expression_valid_rebinding_iteration_variable(self):
|
||||||
|
# This test covers that we can reassign variables
|
||||||
|
# that are not directly assigned in the
|
||||||
|
# iterable part of a comprehension.
|
||||||
|
cases = [
|
||||||
|
# Regression tests from https://github.com/python/cpython/issues/87447
|
||||||
|
("Complex expression: c",
|
||||||
|
"{0}(c := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
|
||||||
|
("Complex expression: d",
|
||||||
|
"{0}(d := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
|
||||||
|
("Complex expression: e",
|
||||||
|
"{0}(e := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
|
||||||
|
("Complex expression: f",
|
||||||
|
"{0}(f := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
|
||||||
|
("Complex expression: g",
|
||||||
|
"{0}(g := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
|
||||||
|
("Complex expression: h",
|
||||||
|
"{0}(h := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
|
||||||
|
("Complex expression: i",
|
||||||
|
"{0}(i := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
|
||||||
|
("Complex expression: j",
|
||||||
|
"{0}(j := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
|
||||||
|
]
|
||||||
|
for test_case, code in cases:
|
||||||
|
for lpar, rpar in [('(', ')'), ('[', ']'), ('{', '}')]:
|
||||||
|
code = code.format(lpar, rpar)
|
||||||
|
with self.subTest(case=test_case, lpar=lpar, rpar=rpar):
|
||||||
|
# Names used in snippets are not defined,
|
||||||
|
# but we are fine with it: just must not be a SyntaxError.
|
||||||
|
# Names used in snippets are not defined,
|
||||||
|
# but we are fine with it: just must not be a SyntaxError.
|
||||||
|
with self.assertRaises(NameError):
|
||||||
|
exec(code, {}) # Module scope
|
||||||
|
with self.assertRaises(NameError):
|
||||||
|
exec(code, {}, {}) # Class scope
|
||||||
|
exec(f"lambda: {code}", {}) # Function scope
|
||||||
|
|
||||||
|
def test_named_expression_invalid_rebinding_iteration_variable(self):
|
||||||
|
# This test covers that we cannot reassign variables
|
||||||
|
# that are directly assigned in the iterable part of a comprehension.
|
||||||
|
cases = [
|
||||||
|
# Regression tests from https://github.com/python/cpython/issues/87447
|
||||||
|
("Complex expression: a", "a",
|
||||||
|
"{0}(a := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
|
||||||
|
("Complex expression: b", "b",
|
||||||
|
"{0}(b := 1) for a, (*b, c[d+e::f(g)], h.i) in j{1}"),
|
||||||
|
]
|
||||||
|
for test_case, target, code in cases:
|
||||||
|
msg = f"assignment expression cannot rebind comprehension iteration variable '{target}'"
|
||||||
|
for lpar, rpar in [('(', ')'), ('[', ']'), ('{', '}')]:
|
||||||
|
code = code.format(lpar, rpar)
|
||||||
|
with self.subTest(case=test_case, lpar=lpar, rpar=rpar):
|
||||||
|
# Names used in snippets are not defined,
|
||||||
|
# but we are fine with it: just must not be a SyntaxError.
|
||||||
|
# Names used in snippets are not defined,
|
||||||
|
# but we are fine with it: just must not be a SyntaxError.
|
||||||
|
with self.assertRaisesRegex(SyntaxError, msg):
|
||||||
|
exec(code, {}) # Module scope
|
||||||
|
with self.assertRaisesRegex(SyntaxError, msg):
|
||||||
|
exec(code, {}, {}) # Class scope
|
||||||
|
with self.assertRaisesRegex(SyntaxError, msg):
|
||||||
|
exec(f"lambda: {code}", {}) # Function scope
|
||||||
|
|
||||||
def test_named_expression_invalid_rebinding_list_comprehension_iteration_variable(self):
|
def test_named_expression_invalid_rebinding_list_comprehension_iteration_variable(self):
|
||||||
cases = [
|
cases = [
|
||||||
("Local reuse", 'i', "[i := 0 for i in range(5)]"),
|
("Local reuse", 'i', "[i := 0 for i in range(5)]"),
|
||||||
|
@ -129,7 +192,11 @@ class NamedExpressionInvalidTest(unittest.TestCase):
|
||||||
msg = f"assignment expression cannot rebind comprehension iteration variable '{target}'"
|
msg = f"assignment expression cannot rebind comprehension iteration variable '{target}'"
|
||||||
with self.subTest(case=case):
|
with self.subTest(case=case):
|
||||||
with self.assertRaisesRegex(SyntaxError, msg):
|
with self.assertRaisesRegex(SyntaxError, msg):
|
||||||
exec(code, {}, {})
|
exec(code, {}) # Module scope
|
||||||
|
with self.assertRaisesRegex(SyntaxError, msg):
|
||||||
|
exec(code, {}, {}) # Class scope
|
||||||
|
with self.assertRaisesRegex(SyntaxError, msg):
|
||||||
|
exec(f"lambda: {code}", {}) # Function scope
|
||||||
|
|
||||||
def test_named_expression_invalid_rebinding_list_comprehension_inner_loop(self):
|
def test_named_expression_invalid_rebinding_list_comprehension_inner_loop(self):
|
||||||
cases = [
|
cases = [
|
||||||
|
@ -178,12 +245,21 @@ class NamedExpressionInvalidTest(unittest.TestCase):
|
||||||
("Unreachable reuse", 'i', "{False or (i:=0) for i in range(5)}"),
|
("Unreachable reuse", 'i', "{False or (i:=0) for i in range(5)}"),
|
||||||
("Unreachable nested reuse", 'i',
|
("Unreachable nested reuse", 'i',
|
||||||
"{(i, j) for i in range(5) for j in range(5) if True or (i:=10)}"),
|
"{(i, j) for i in range(5) for j in range(5) if True or (i:=10)}"),
|
||||||
|
# Regression tests from https://github.com/python/cpython/issues/87447
|
||||||
|
("Complex expression: a", "a",
|
||||||
|
"{(a := 1) for a, (*b, c[d+e::f(g)], h.i) in j}"),
|
||||||
|
("Complex expression: b", "b",
|
||||||
|
"{(b := 1) for a, (*b, c[d+e::f(g)], h.i) in j}"),
|
||||||
]
|
]
|
||||||
for case, target, code in cases:
|
for case, target, code in cases:
|
||||||
msg = f"assignment expression cannot rebind comprehension iteration variable '{target}'"
|
msg = f"assignment expression cannot rebind comprehension iteration variable '{target}'"
|
||||||
with self.subTest(case=case):
|
with self.subTest(case=case):
|
||||||
with self.assertRaisesRegex(SyntaxError, msg):
|
with self.assertRaisesRegex(SyntaxError, msg):
|
||||||
exec(code, {}, {})
|
exec(code, {}) # Module scope
|
||||||
|
with self.assertRaisesRegex(SyntaxError, msg):
|
||||||
|
exec(code, {}, {}) # Class scope
|
||||||
|
with self.assertRaisesRegex(SyntaxError, msg):
|
||||||
|
exec(f"lambda: {code}", {}) # Function scope
|
||||||
|
|
||||||
def test_named_expression_invalid_rebinding_set_comprehension_inner_loop(self):
|
def test_named_expression_invalid_rebinding_set_comprehension_inner_loop(self):
|
||||||
cases = [
|
cases = [
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
Fix :exc:`SyntaxError` on comprehension rebind checking with names that are
|
||||||
|
not actually redefined.
|
||||||
|
|
||||||
|
Now reassigning ``b`` in ``[(b := 1) for a, b.prop in some_iter]`` is allowed.
|
||||||
|
Reassigning ``a`` is still disallowed as per :pep:`572`.
|
|
@ -1488,7 +1488,8 @@ symtable_extend_namedexpr_scope(struct symtable *st, expr_ty e)
|
||||||
*/
|
*/
|
||||||
if (ste->ste_comprehension) {
|
if (ste->ste_comprehension) {
|
||||||
long target_in_scope = _PyST_GetSymbol(ste, target_name);
|
long target_in_scope = _PyST_GetSymbol(ste, target_name);
|
||||||
if (target_in_scope & DEF_COMP_ITER) {
|
if ((target_in_scope & DEF_COMP_ITER) &&
|
||||||
|
(target_in_scope & DEF_LOCAL)) {
|
||||||
PyErr_Format(PyExc_SyntaxError, NAMED_EXPR_COMP_CONFLICT, target_name);
|
PyErr_Format(PyExc_SyntaxError, NAMED_EXPR_COMP_CONFLICT, target_name);
|
||||||
PyErr_RangedSyntaxLocationObject(st->st_filename,
|
PyErr_RangedSyntaxLocationObject(st->st_filename,
|
||||||
e->lineno,
|
e->lineno,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue