Skip to content

Commit cc3a0c9

Browse files
authored
[3.15] gh-160299: Preserve references from inlined comprehensions (GH-156819) (#160413)
Adapt the reference-flag propagation from GH-156819 to fix gh-160299 without backporting the comprehensive changes to comprehension scope representation and compilation.
1 parent 073f4f1 commit cc3a0c9

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
@@ -617,6 +617,58 @@ def test_filter_syntax_warnings_by_module(self):
617617

618618

619619
class ComprehensionTests(unittest.TestCase):
620+
def test_referenced_in_comprehension(self):
621+
comps = [
622+
"[x + k + local for x in xs]",
623+
"{x + k + local for x in xs}",
624+
"{x: k + local for x in xs}",
625+
"[[x + k + local for x in xs] for _ in xs]",
626+
]
627+
for comp in comps:
628+
with self.subTest(comp=comp):
629+
st = symtable.symtable(
630+
"def f(xs, k):\n"
631+
" local = 1\n"
632+
f" return {comp}\n", "?", "exec")
633+
f = find_block(st, "f")
634+
self.assertTrue(f.lookup("k").is_referenced())
635+
self.assertTrue(f.lookup("local").is_referenced())
636+
self.assertEqual(f.get_children(), [])
637+
638+
def test_nonlocal_referenced_in_comprehension(self):
639+
st = symtable.symtable(
640+
"def outer(k):\n"
641+
" def f(xs):\n"
642+
" nonlocal k\n"
643+
" return [x + k for x in xs]\n", "?", "exec")
644+
f = find_block(find_block(st, "outer"), "f")
645+
self.assertTrue(f.lookup("k").is_referenced())
646+
self.assertTrue(f.lookup("k").is_nonlocal())
647+
648+
def test_not_referenced_in_comprehension(self):
649+
comps = [
650+
"[k for k in xs]", # The iteration variable shadows the parameter.
651+
"[[k for _ in ()] for k in xs]", # Shadowing in a nested comprehension.
652+
"[(lambda: k) for x in xs]", # The load is in a nested function.
653+
"(x + k for x in xs)", # Generator expressions are not inlined.
654+
]
655+
for comp in comps:
656+
with self.subTest(comp=comp):
657+
st = symtable.symtable(
658+
"def f(xs, k):\n"
659+
f" return {comp}\n", "?", "exec")
660+
self.assertFalse(find_block(st, "f").lookup("k").is_referenced())
661+
662+
def test_class_local_not_referenced_in_comprehension(self):
663+
st = symtable.symtable(
664+
"def outer(k):\n"
665+
" class C:\n"
666+
" k = 1\n"
667+
" values = [k for _ in ()]\n", "?", "exec")
668+
cls = find_block(find_block(st, "outer"), "C")
669+
# The comprehension refers to outer's k, not the class attribute.
670+
self.assertFalse(cls.lookup("k").is_referenced())
671+
620672
def get_identifiers_recursive(self, st, res):
621673
res.extend(st.get_identifiers())
622674
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
@@ -879,6 +879,20 @@ inline_comprehension(PySTEntryObject *ste, PySTEntryObject *comp,
879879
if (flags == -1 && PyErr_Occurred()) {
880880
return 0;
881881
}
882+
// Propagate uses of enclosing bindings, but not class locals,
883+
// which are not visible to comprehensions.
884+
if (scope == FREE && ste->ste_type != ClassBlock &&
885+
(comp_flags & USE) && !(flags & USE)) {
886+
PyObject *v_flags = PyLong_FromLong(flags | USE);
887+
if (v_flags == NULL) {
888+
return 0;
889+
}
890+
int ok = PyDict_SetItem(ste->ste_symbols, k, v_flags);
891+
Py_DECREF(v_flags);
892+
if (ok < 0) {
893+
return 0;
894+
}
895+
}
882896
if ((flags & DEF_BOUND) && ste->ste_type != ClassBlock) {
883897
// free vars in comprehension that are locals in outer scope can
884898
// now simply be locals, unless they are free in comp children,

0 commit comments

Comments
 (0)