Skip to content

Commit 18f7a07

Browse files
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 <noreply@anthropic.com>
1 parent 8ca85b5 commit 18f7a07

3 files changed

Lines changed: 109 additions & 1 deletion

File tree

Lib/test/test_symtable.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,76 @@ def test_loopvar_in_only_one_scope(self):
554554
self.assertEqual(len([x for x in ids if x == 'x']), 1)
555555

556556

557+
class EntryAttributeTests(unittest.TestCase):
558+
"""Scope attributes of the low-level symbol table entries."""
559+
560+
@classmethod
561+
def setUpClass(cls):
562+
import _symtable
563+
cls.TYPE_ANNOTATION = _symtable.TYPE_ANNOTATION
564+
cls.top = _symtable.symtable(
565+
"async def agen(): yield\n"
566+
"def gen(): yield\n"
567+
"def f(): pass\n"
568+
"x: int = 1\n"
569+
"class C:\n"
570+
" def m(self): return __class__\n"
571+
" if x:\n"
572+
" y: int = 2\n"
573+
"class P:\n"
574+
" pass\n",
575+
"<test>", "exec")
576+
577+
@staticmethod
578+
def find(table, name):
579+
return next(c for c in table.children if c.name == name)
580+
581+
def test_is_generator(self):
582+
self.assertTrue(self.find(self.top, "agen").is_generator)
583+
self.assertTrue(self.find(self.top, "gen").is_generator)
584+
self.assertFalse(self.find(self.top, "f").is_generator)
585+
586+
def test_is_coroutine(self):
587+
self.assertTrue(self.find(self.top, "agen").is_coroutine)
588+
self.assertFalse(self.find(self.top, "gen").is_coroutine)
589+
self.assertFalse(self.find(self.top, "f").is_coroutine)
590+
591+
def test_has_annotations(self):
592+
self.assertTrue(self.top.has_annotations)
593+
self.assertTrue(self.find(self.top, "C").has_annotations)
594+
self.assertFalse(self.find(self.top, "f").has_annotations)
595+
596+
def test_has_conditional_annotations(self):
597+
# Module annotations are always conditional.
598+
self.assertTrue(self.top.has_conditional_annotations)
599+
self.assertTrue(
600+
self.find(self.top, "C").has_conditional_annotations)
601+
self.assertFalse(
602+
self.find(self.top, "P").has_conditional_annotations)
603+
604+
def test_needs_class_closure(self):
605+
self.assertTrue(self.find(self.top, "C").needs_class_closure)
606+
self.assertFalse(self.find(self.top, "P").needs_class_closure)
607+
608+
def test_needs_classdict(self):
609+
self.assertTrue(self.find(self.top, "C").needs_classdict)
610+
self.assertFalse(self.find(self.top, "P").needs_classdict)
611+
612+
def test_annotation_block(self):
613+
anno = self.top.annotation_block
614+
self.assertIsNotNone(anno)
615+
self.assertIn(anno, self.top.children)
616+
self.assertEqual(anno.type, self.TYPE_ANNOTATION)
617+
self.assertIsNone(self.find(self.top, "f").annotation_block)
618+
self.assertIsNotNone(self.find(self.top, "C").annotation_block)
619+
620+
def test_can_see_class_scope(self):
621+
C = self.find(self.top, "C")
622+
self.assertTrue(C.annotation_block.can_see_class_scope)
623+
self.assertFalse(self.top.annotation_block.can_see_class_scope)
624+
self.assertFalse(self.find(self.top, "f").can_see_class_scope)
625+
626+
557627
class CommandLineTest(unittest.TestCase):
558628
maxDiff = None
559629

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Symbol table entries returned by the internal :mod:`!_symtable` module
2+
now expose the scope attributes computed by the analysis:
3+
``is_generator``, ``is_coroutine``, ``has_annotations``,
4+
``has_conditional_annotations``, ``needs_class_closure``,
5+
``needs_classdict``, ``can_see_class_scope`` and ``annotation_block``.

Python/symtable.c

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,45 @@ static PyMemberDef ste_memberlist[] = {
205205
{"symbols", _Py_T_OBJECT, OFF(ste_symbols), Py_READONLY},
206206
{"varnames", _Py_T_OBJECT, OFF(ste_varnames), Py_READONLY},
207207
{"children", _Py_T_OBJECT, OFF(ste_children), Py_READONLY},
208+
{"annotation_block", _Py_T_OBJECT, OFF(ste_annotation_block), Py_READONLY},
208209
{"nested", Py_T_INT, OFF(ste_nested), Py_READONLY},
209210
{"type", Py_T_INT, OFF(ste_type), Py_READONLY},
210211
{"lineno", Py_T_INT, OFF(ste_loc.lineno), Py_READONLY},
211212
{NULL}
212213
};
213214

215+
/* The flag fields are C bitfields, which PyMemberDef cannot describe;
216+
expose them through getters. */
217+
#define STE_BOOL_GETTER(FIELD) \
218+
static PyObject * \
219+
ste_get_ ## FIELD(PyObject *op, void *Py_UNUSED(closure)) \
220+
{ \
221+
PySTEntryObject *ste = (PySTEntryObject *)op; \
222+
return PyBool_FromLong(ste->ste_ ## FIELD); \
223+
}
224+
225+
STE_BOOL_GETTER(generator)
226+
STE_BOOL_GETTER(coroutine)
227+
STE_BOOL_GETTER(annotations_used)
228+
STE_BOOL_GETTER(needs_class_closure)
229+
STE_BOOL_GETTER(needs_classdict)
230+
STE_BOOL_GETTER(can_see_class_scope)
231+
STE_BOOL_GETTER(has_conditional_annotations)
232+
233+
#undef STE_BOOL_GETTER
234+
235+
static PyGetSetDef ste_getsetlist[] = {
236+
{"is_generator", ste_get_generator, NULL, NULL, NULL},
237+
{"is_coroutine", ste_get_coroutine, NULL, NULL, NULL},
238+
{"has_annotations", ste_get_annotations_used, NULL, NULL, NULL},
239+
{"needs_class_closure", ste_get_needs_class_closure, NULL, NULL, NULL},
240+
{"needs_classdict", ste_get_needs_classdict, NULL, NULL, NULL},
241+
{"can_see_class_scope", ste_get_can_see_class_scope, NULL, NULL, NULL},
242+
{"has_conditional_annotations", ste_get_has_conditional_annotations,
243+
NULL, NULL, NULL},
244+
{NULL}
245+
};
246+
214247
PyTypeObject PySTEntry_Type = {
215248
PyVarObject_HEAD_INIT(&PyType_Type, 0)
216249
"symtable entry",
@@ -241,7 +274,7 @@ PyTypeObject PySTEntry_Type = {
241274
0, /* tp_iternext */
242275
0, /* tp_methods */
243276
ste_memberlist, /* tp_members */
244-
0, /* tp_getset */
277+
ste_getsetlist, /* tp_getset */
245278
0, /* tp_base */
246279
0, /* tp_dict */
247280
0, /* tp_descr_get */

0 commit comments

Comments
 (0)