Skip to content

Commit 3706eef

Browse files
miss-islingtoncarljmJelleZijlstraEclips4
authored
[3.12] gh-118513: Fix sibling comprehensions with a name bound in one and global in the other (GH-118526) (#118548)
gh-118513: Fix sibling comprehensions with a name bound in one and global in the other (GH-118526) (cherry picked from commit c8deb1e) Co-authored-by: Carl Meyer <[email protected]> Co-authored-by: Jelle Zijlstra <[email protected]> Co-authored-by: Kirill Podoprigora <[email protected]>
1 parent a7f495c commit 3706eef

File tree

3 files changed

+57
-39
lines changed

3 files changed

+57
-39
lines changed

Lib/test/test_listcomps.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,20 @@ def test_code_replace_extended_arg(self):
666666
self._check_in_scopes(code, expected)
667667
self._check_in_scopes(code, expected, exec_func=self._replacing_exec)
668668

669+
def test_multiple_comprehension_name_reuse(self):
670+
code = """
671+
[x for x in [1]]
672+
y = [x for _ in [1]]
673+
"""
674+
self._check_in_scopes(code, {"y": [3]}, ns={"x": 3})
675+
676+
code = """
677+
x = 2
678+
[x for x in [1]]
679+
y = [x for _ in [1]]
680+
"""
681+
self._check_in_scopes(code, {"x": 2, "y": [3]}, ns={"x": 3}, scopes=["class"])
682+
self._check_in_scopes(code, {"x": 2, "y": [2]}, ns={"x": 3}, scopes=["function", "module"])
669683

670684
__test__ = {'doctests' : doctests}
671685

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix incorrect :exc:`UnboundLocalError` when two comprehensions in the same function both reference the same name, and in one comprehension the name is bound while in the other it's an implicit global.

Python/compile.c

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5458,10 +5458,48 @@ push_inlined_comprehension_state(struct compiler *c, location loc,
54585458
while (PyDict_Next(entry->ste_symbols, &pos, &k, &v)) {
54595459
assert(PyLong_Check(v));
54605460
long symbol = PyLong_AS_LONG(v);
5461-
// only values bound in the comprehension (DEF_LOCAL) need to be handled
5462-
// at all; DEF_LOCAL | DEF_NONLOCAL can occur in the case of an
5463-
// assignment expression to a nonlocal in the comprehension, these don't
5464-
// need handling here since they shouldn't be isolated
5461+
long scope = (symbol >> SCOPE_OFFSET) & SCOPE_MASK;
5462+
PyObject *outv = PyDict_GetItemWithError(c->u->u_ste->ste_symbols, k);
5463+
if (outv == NULL) {
5464+
if (PyErr_Occurred()) {
5465+
return ERROR;
5466+
}
5467+
outv = _PyLong_GetZero();
5468+
}
5469+
assert(PyLong_CheckExact(outv));
5470+
long outsc = (PyLong_AS_LONG(outv) >> SCOPE_OFFSET) & SCOPE_MASK;
5471+
// If a name has different scope inside than outside the comprehension,
5472+
// we need to temporarily handle it with the right scope while
5473+
// compiling the comprehension. If it's free in the comprehension
5474+
// scope, no special handling; it should be handled the same as the
5475+
// enclosing scope. (If it's free in outer scope and cell in inner
5476+
// scope, we can't treat it as both cell and free in the same function,
5477+
// but treating it as free throughout is fine; it's *_DEREF
5478+
// either way.)
5479+
if ((scope != outsc && scope != FREE && !(scope == CELL && outsc == FREE))
5480+
|| in_class_block) {
5481+
if (state->temp_symbols == NULL) {
5482+
state->temp_symbols = PyDict_New();
5483+
if (state->temp_symbols == NULL) {
5484+
return ERROR;
5485+
}
5486+
}
5487+
// update the symbol to the in-comprehension version and save
5488+
// the outer version; we'll restore it after running the
5489+
// comprehension
5490+
Py_INCREF(outv);
5491+
if (PyDict_SetItem(c->u->u_ste->ste_symbols, k, v) < 0) {
5492+
Py_DECREF(outv);
5493+
return ERROR;
5494+
}
5495+
if (PyDict_SetItem(state->temp_symbols, k, outv) < 0) {
5496+
Py_DECREF(outv);
5497+
return ERROR;
5498+
}
5499+
Py_DECREF(outv);
5500+
}
5501+
// locals handling for names bound in comprehension (DEF_LOCAL |
5502+
// DEF_NONLOCAL occurs in assignment expression to nonlocal)
54655503
if ((symbol & DEF_LOCAL && !(symbol & DEF_NONLOCAL)) || in_class_block) {
54665504
if (!_PyST_IsFunctionLike(c->u->u_ste)) {
54675505
// non-function scope: override this name to use fast locals
@@ -5481,41 +5519,6 @@ push_inlined_comprehension_state(struct compiler *c, location loc,
54815519
}
54825520
}
54835521
}
5484-
long scope = (symbol >> SCOPE_OFFSET) & SCOPE_MASK;
5485-
PyObject *outv = PyDict_GetItemWithError(c->u->u_ste->ste_symbols, k);
5486-
if (outv == NULL) {
5487-
outv = _PyLong_GetZero();
5488-
}
5489-
assert(PyLong_Check(outv));
5490-
long outsc = (PyLong_AS_LONG(outv) >> SCOPE_OFFSET) & SCOPE_MASK;
5491-
if (scope != outsc && !(scope == CELL && outsc == FREE)) {
5492-
// If a name has different scope inside than outside the
5493-
// comprehension, we need to temporarily handle it with the
5494-
// right scope while compiling the comprehension. (If it's free
5495-
// in outer scope and cell in inner scope, we can't treat it as
5496-
// both cell and free in the same function, but treating it as
5497-
// free throughout is fine; it's *_DEREF either way.)
5498-
5499-
if (state->temp_symbols == NULL) {
5500-
state->temp_symbols = PyDict_New();
5501-
if (state->temp_symbols == NULL) {
5502-
return ERROR;
5503-
}
5504-
}
5505-
// update the symbol to the in-comprehension version and save
5506-
// the outer version; we'll restore it after running the
5507-
// comprehension
5508-
Py_INCREF(outv);
5509-
if (PyDict_SetItem(c->u->u_ste->ste_symbols, k, v) < 0) {
5510-
Py_DECREF(outv);
5511-
return ERROR;
5512-
}
5513-
if (PyDict_SetItem(state->temp_symbols, k, outv) < 0) {
5514-
Py_DECREF(outv);
5515-
return ERROR;
5516-
}
5517-
Py_DECREF(outv);
5518-
}
55195522
// local names bound in comprehension must be isolated from
55205523
// outer scope; push existing value (which may be NULL if
55215524
// not defined) on stack

0 commit comments

Comments
 (0)