From cc97a7fd99e6ec4c10d5805aaac7560880250c34 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 23 Oct 2026 10:41:48 -0700 Subject: [PATCH] [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 cc3a0c9d3a260ed78c9751d40af4041bb826bf75) --- Lib/test/test_symtable.py | 52 +++++++++++++++++++ ...10-22-12-00-00.gh-issue-160299.CompUse.rst | 2 + Python/symtable.c | 14 +++++ 3 files changed, 68 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-22-12-00-00.gh-issue-160299.CompUse.rst diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py index 9537de3756fd32f..3e18dd5e697b7d6 100644 --- a/Lib/test/test_symtable.py +++ b/Lib/test/test_symtable.py @@ -536,6 +536,58 @@ def test__symtable_refleak(self): class ComprehensionTests(unittest.TestCase): + def test_referenced_in_comprehension(self): + comps = [ + "[x + k + local for x in xs]", + "{x + k + local for x in xs}", + "{x: k + local for x in xs}", + "[[x + k + local for x in xs] for _ in xs]", + ] + for comp in comps: + with self.subTest(comp=comp): + st = symtable.symtable( + "def f(xs, k):\n" + " local = 1\n" + f" return {comp}\n", "?", "exec") + f = find_block(st, "f") + self.assertTrue(f.lookup("k").is_referenced()) + self.assertTrue(f.lookup("local").is_referenced()) + self.assertEqual(f.get_children(), []) + + def test_nonlocal_referenced_in_comprehension(self): + st = symtable.symtable( + "def outer(k):\n" + " def f(xs):\n" + " nonlocal k\n" + " return [x + k for x in xs]\n", "?", "exec") + f = find_block(find_block(st, "outer"), "f") + self.assertTrue(f.lookup("k").is_referenced()) + self.assertTrue(f.lookup("k").is_nonlocal()) + + def test_not_referenced_in_comprehension(self): + comps = [ + "[k for k in xs]", # The iteration variable shadows the parameter. + "[[k for _ in ()] for k in xs]", # Shadowing in a nested comprehension. + "[(lambda: k) for x in xs]", # The load is in a nested function. + "(x + k for x in xs)", # Generator expressions are not inlined. + ] + for comp in comps: + with self.subTest(comp=comp): + st = symtable.symtable( + "def f(xs, k):\n" + f" return {comp}\n", "?", "exec") + self.assertFalse(find_block(st, "f").lookup("k").is_referenced()) + + def test_class_local_not_referenced_in_comprehension(self): + st = symtable.symtable( + "def outer(k):\n" + " class C:\n" + " k = 1\n" + " values = [k for _ in ()]\n", "?", "exec") + cls = find_block(find_block(st, "outer"), "C") + # The comprehension refers to outer's k, not the class attribute. + self.assertFalse(cls.lookup("k").is_referenced()) + def get_identifiers_recursive(self, st, res): res.extend(st.get_identifiers()) for ch in st.get_children(): diff --git a/Misc/NEWS.d/next/Library/2026-10-22-12-00-00.gh-issue-160299.CompUse.rst b/Misc/NEWS.d/next/Library/2026-10-22-12-00-00.gh-issue-160299.CompUse.rst new file mode 100644 index 000000000000000..6bdfaa8a7ba1aad --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-22-12-00-00.gh-issue-160299.CompUse.rst @@ -0,0 +1,2 @@ +Fix :meth:`symtable.Symbol.is_referenced` for local and nonlocal symbols in +function scopes when their only uses are in inlined comprehensions. diff --git a/Python/symtable.c b/Python/symtable.c index 442214d1e34d6cc..d4f621a474ed950 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -876,6 +876,20 @@ inline_comprehension(PySTEntryObject *ste, PySTEntryObject *comp, if (flags == -1 && PyErr_Occurred()) { return 0; } + // Propagate uses of enclosing bindings, but not class locals, + // which are not visible to comprehensions. + if (scope == FREE && ste->ste_type != ClassBlock && + (comp_flags & USE) && !(flags & USE)) { + PyObject *v_flags = PyLong_FromLong(flags | USE); + if (v_flags == NULL) { + return 0; + } + int ok = PyDict_SetItem(ste->ste_symbols, k, v_flags); + Py_DECREF(v_flags); + if (ok < 0) { + return 0; + } + } if ((flags & DEF_BOUND) && ste->ste_type != ClassBlock) { // free vars in comprehension that are locals in outer scope can // now simply be locals, unless they are free in comp children,