Skip to content

Commit cc97a7f

Browse files
committed
[3.14] gh-160299: Preserve references from inlined comprehensions (GH-160413)
Adapt the reference-flag propagation from GH-156819 to fix gh-160299 without backporting the comprehensive changes to comprehension scope representation and compilation. (cherry picked from commit cc3a0c9)
1 parent e456b57 commit cc97a7f

3 files changed

Lines changed: 68 additions & 0 deletions

File tree

‎Lib/test/test_symtable.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,58 @@ def test__symtable_refleak(self):
536536

537537

538538
class ComprehensionTests(unittest.TestCase):
539+
def test_referenced_in_comprehension(self):
540+
comps = [
541+
"[x + k + local for x in xs]",
542+
"{x + k + local for x in xs}",
543+
"{x: k + local for x in xs}",
544+
"[[x + k + local for x in xs] for _ in xs]",
545+
]
546+
for comp in comps:
547+
with self.subTest(comp=comp):
548+
st = symtable.symtable(
549+
"def f(xs, k):\n"
550+
" local = 1\n"
551+
f" return {comp}\n", "?", "exec")
552+
f = find_block(st, "f")
553+
self.assertTrue(f.lookup("k").is_referenced())
554+
self.assertTrue(f.lookup("local").is_referenced())
555+
self.assertEqual(f.get_children(), [])
556+
557+
def test_nonlocal_referenced_in_comprehension(self):
558+
st = symtable.symtable(
559+
"def outer(k):\n"
560+
" def f(xs):\n"
561+
" nonlocal k\n"
562+
" return [x + k for x in xs]\n", "?", "exec")
563+
f = find_block(find_block(st, "outer"), "f")
564+
self.assertTrue(f.lookup("k").is_referenced())
565+
self.assertTrue(f.lookup("k").is_nonlocal())
566+
567+
def test_not_referenced_in_comprehension(self):
568+
comps = [
569+
"[k for k in xs]", # The iteration variable shadows the parameter.
570+
"[[k for _ in ()] for k in xs]", # Shadowing in a nested comprehension.
571+
"[(lambda: k) for x in xs]", # The load is in a nested function.
572+
"(x + k for x in xs)", # Generator expressions are not inlined.
573+
]
574+
for comp in comps:
575+
with self.subTest(comp=comp):
576+
st = symtable.symtable(
577+
"def f(xs, k):\n"
578+
f" return {comp}\n", "?", "exec")
579+
self.assertFalse(find_block(st, "f").lookup("k").is_referenced())
580+
581+
def test_class_local_not_referenced_in_comprehension(self):
582+
st = symtable.symtable(
583+
"def outer(k):\n"
584+
" class C:\n"
585+
" k = 1\n"
586+
" values = [k for _ in ()]\n", "?", "exec")
587+
cls = find_block(find_block(st, "outer"), "C")
588+
# The comprehension refers to outer's k, not the class attribute.
589+
self.assertFalse(cls.lookup("k").is_referenced())
590+
539591
def get_identifiers_recursive(self, st, res):
540592
res.extend(st.get_identifiers())
541593
for ch in st.get_children():
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :meth:`symtable.Symbol.is_referenced` for local and nonlocal symbols in
2+
function scopes when their only uses are in inlined comprehensions.

‎Python/symtable.c‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,20 @@ inline_comprehension(PySTEntryObject *ste, PySTEntryObject *comp,
876876
if (flags == -1 && PyErr_Occurred()) {
877877
return 0;
878878
}
879+
// Propagate uses of enclosing bindings, but not class locals,
880+
// which are not visible to comprehensions.
881+
if (scope == FREE && ste->ste_type != ClassBlock &&
882+
(comp_flags & USE) && !(flags & USE)) {
883+
PyObject *v_flags = PyLong_FromLong(flags | USE);
884+
if (v_flags == NULL) {
885+
return 0;
886+
}
887+
int ok = PyDict_SetItem(ste->ste_symbols, k, v_flags);
888+
Py_DECREF(v_flags);
889+
if (ok < 0) {
890+
return 0;
891+
}
892+
}
879893
if ((flags & DEF_BOUND) && ste->ste_type != ClassBlock) {
880894
// free vars in comprehension that are locals in outer scope can
881895
// now simply be locals, unless they are free in comp children,

0 commit comments

Comments
 (0)