From 18f7a07bf1fc05da60e66ff8cbdee6fc10bfce81 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 17 Jul 2026 19:08:05 +0300 Subject: [PATCH] gh-153849: Expose scope attributes on symtable entries The symbol table analysis computes a number of per-scope attributes that the compiler relies on, but did not expose them. Add read-only attributes to the symbol table entries: is_generator, is_coroutine, has_annotations, has_conditional_annotations, needs_class_closure, needs_classdict, can_see_class_scope and annotation_block. Co-Authored-By: Claude Fable 5 --- Lib/test/test_symtable.py | 70 +++++++++++++++++++ ...-07-17-17-10-00.gh-issue-153849.wQx3Rm.rst | 5 ++ Python/symtable.c | 35 +++++++++- 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-17-17-10-00.gh-issue-153849.wQx3Rm.rst diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py index 8c03420c4c5e4b..0a1970d09114fa 100644 --- a/Lib/test/test_symtable.py +++ b/Lib/test/test_symtable.py @@ -554,6 +554,76 @@ def test_loopvar_in_only_one_scope(self): self.assertEqual(len([x for x in ids if x == 'x']), 1) +class EntryAttributeTests(unittest.TestCase): + """Scope attributes of the low-level symbol table entries.""" + + @classmethod + def setUpClass(cls): + import _symtable + cls.TYPE_ANNOTATION = _symtable.TYPE_ANNOTATION + cls.top = _symtable.symtable( + "async def agen(): yield\n" + "def gen(): yield\n" + "def f(): pass\n" + "x: int = 1\n" + "class C:\n" + " def m(self): return __class__\n" + " if x:\n" + " y: int = 2\n" + "class P:\n" + " pass\n", + "", "exec") + + @staticmethod + def find(table, name): + return next(c for c in table.children if c.name == name) + + def test_is_generator(self): + self.assertTrue(self.find(self.top, "agen").is_generator) + self.assertTrue(self.find(self.top, "gen").is_generator) + self.assertFalse(self.find(self.top, "f").is_generator) + + def test_is_coroutine(self): + self.assertTrue(self.find(self.top, "agen").is_coroutine) + self.assertFalse(self.find(self.top, "gen").is_coroutine) + self.assertFalse(self.find(self.top, "f").is_coroutine) + + def test_has_annotations(self): + self.assertTrue(self.top.has_annotations) + self.assertTrue(self.find(self.top, "C").has_annotations) + self.assertFalse(self.find(self.top, "f").has_annotations) + + def test_has_conditional_annotations(self): + # Module annotations are always conditional. + self.assertTrue(self.top.has_conditional_annotations) + self.assertTrue( + self.find(self.top, "C").has_conditional_annotations) + self.assertFalse( + self.find(self.top, "P").has_conditional_annotations) + + def test_needs_class_closure(self): + self.assertTrue(self.find(self.top, "C").needs_class_closure) + self.assertFalse(self.find(self.top, "P").needs_class_closure) + + def test_needs_classdict(self): + self.assertTrue(self.find(self.top, "C").needs_classdict) + self.assertFalse(self.find(self.top, "P").needs_classdict) + + def test_annotation_block(self): + anno = self.top.annotation_block + self.assertIsNotNone(anno) + self.assertIn(anno, self.top.children) + self.assertEqual(anno.type, self.TYPE_ANNOTATION) + self.assertIsNone(self.find(self.top, "f").annotation_block) + self.assertIsNotNone(self.find(self.top, "C").annotation_block) + + def test_can_see_class_scope(self): + C = self.find(self.top, "C") + self.assertTrue(C.annotation_block.can_see_class_scope) + self.assertFalse(self.top.annotation_block.can_see_class_scope) + self.assertFalse(self.find(self.top, "f").can_see_class_scope) + + class CommandLineTest(unittest.TestCase): maxDiff = None diff --git a/Misc/NEWS.d/next/Library/2026-07-17-17-10-00.gh-issue-153849.wQx3Rm.rst b/Misc/NEWS.d/next/Library/2026-07-17-17-10-00.gh-issue-153849.wQx3Rm.rst new file mode 100644 index 00000000000000..fb6c8f0670e108 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-17-17-10-00.gh-issue-153849.wQx3Rm.rst @@ -0,0 +1,5 @@ +Symbol table entries returned by the internal :mod:`!_symtable` module +now expose the scope attributes computed by the analysis: +``is_generator``, ``is_coroutine``, ``has_annotations``, +``has_conditional_annotations``, ``needs_class_closure``, +``needs_classdict``, ``can_see_class_scope`` and ``annotation_block``. diff --git a/Python/symtable.c b/Python/symtable.c index e3e89ab403a607..02a83cc2826465 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -205,12 +205,45 @@ static PyMemberDef ste_memberlist[] = { {"symbols", _Py_T_OBJECT, OFF(ste_symbols), Py_READONLY}, {"varnames", _Py_T_OBJECT, OFF(ste_varnames), Py_READONLY}, {"children", _Py_T_OBJECT, OFF(ste_children), Py_READONLY}, + {"annotation_block", _Py_T_OBJECT, OFF(ste_annotation_block), Py_READONLY}, {"nested", Py_T_INT, OFF(ste_nested), Py_READONLY}, {"type", Py_T_INT, OFF(ste_type), Py_READONLY}, {"lineno", Py_T_INT, OFF(ste_loc.lineno), Py_READONLY}, {NULL} }; +/* The flag fields are C bitfields, which PyMemberDef cannot describe; + expose them through getters. */ +#define STE_BOOL_GETTER(FIELD) \ +static PyObject * \ +ste_get_ ## FIELD(PyObject *op, void *Py_UNUSED(closure)) \ +{ \ + PySTEntryObject *ste = (PySTEntryObject *)op; \ + return PyBool_FromLong(ste->ste_ ## FIELD); \ +} + +STE_BOOL_GETTER(generator) +STE_BOOL_GETTER(coroutine) +STE_BOOL_GETTER(annotations_used) +STE_BOOL_GETTER(needs_class_closure) +STE_BOOL_GETTER(needs_classdict) +STE_BOOL_GETTER(can_see_class_scope) +STE_BOOL_GETTER(has_conditional_annotations) + +#undef STE_BOOL_GETTER + +static PyGetSetDef ste_getsetlist[] = { + {"is_generator", ste_get_generator, NULL, NULL, NULL}, + {"is_coroutine", ste_get_coroutine, NULL, NULL, NULL}, + {"has_annotations", ste_get_annotations_used, NULL, NULL, NULL}, + {"needs_class_closure", ste_get_needs_class_closure, NULL, NULL, NULL}, + {"needs_classdict", ste_get_needs_classdict, NULL, NULL, NULL}, + {"can_see_class_scope", ste_get_can_see_class_scope, NULL, NULL, NULL}, + {"has_conditional_annotations", ste_get_has_conditional_annotations, + NULL, NULL, NULL}, + {NULL} +}; + PyTypeObject PySTEntry_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) "symtable entry", @@ -241,7 +274,7 @@ PyTypeObject PySTEntry_Type = { 0, /* tp_iternext */ 0, /* tp_methods */ ste_memberlist, /* tp_members */ - 0, /* tp_getset */ + ste_getsetlist, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */