Skip to content

Commit 755dab7

Browse files
picnixzcarljm
andauthored
gh-120029: make symtable.Symbol.__repr__ correctly reflect the compiler's flags, add methods (#120099)
Expose :class:`symtable.Symbol` methods :meth:`~symtable.Symbol.is_free_class`, :meth:`~symtable.Symbol.is_comp_iter` and :meth:`~symtable.Symbol.is_comp_cell`. --------- Co-authored-by: Carl Meyer <carl@oddbird.net>
1 parent 7dd8c37 commit 755dab7

File tree

7 files changed

+100
-6
lines changed

7 files changed

+100
-6
lines changed

Doc/library/symtable.rst

+34
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ Examining Symbol Tables
155155

156156
Return ``True`` if the symbol is a type parameter.
157157

158+
.. versionadded:: 3.14
159+
158160
.. method:: is_global()
159161

160162
Return ``True`` if the symbol is global.
@@ -182,10 +184,42 @@ Examining Symbol Tables
182184
Return ``True`` if the symbol is referenced in its block, but not assigned
183185
to.
184186

187+
.. method:: is_free_class()
188+
189+
Return *True* if a class-scoped symbol is free from
190+
the perspective of a method.
191+
192+
Consider the following example::
193+
194+
def f():
195+
x = 1 # function-scoped
196+
class C:
197+
x = 2 # class-scoped
198+
def method(self):
199+
return x
200+
201+
In this example, the class-scoped symbol ``x`` is considered to
202+
be free from the perspective of ``C.method``, thereby allowing
203+
the latter to return *1* at runtime and not *2*.
204+
205+
.. versionadded:: 3.14
206+
185207
.. method:: is_assigned()
186208

187209
Return ``True`` if the symbol is assigned to in its block.
188210

211+
.. method:: is_comp_iter()
212+
213+
Return ``True`` if the symbol is a comprehension iteration variable.
214+
215+
.. versionadded:: 3.14
216+
217+
.. method:: is_comp_cell()
218+
219+
Return ``True`` if the symbol is a cell in an inlined comprehension.
220+
221+
.. versionadded:: 3.14
222+
189223
.. method:: is_namespace()
190224

191225
Return ``True`` if name binding introduces new namespace.

Doc/whatsnew/3.14.rst

+11
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,17 @@ os
100100
by :func:`os.unsetenv`, or made outside Python in the same process.
101101
(Contributed by Victor Stinner in :gh:`120057`.)
102102

103+
symtable
104+
--------
105+
106+
* Expose the following :class:`symtable.Symbol` methods:
107+
108+
* :meth:`~symtable.Symbol.is_free_class`
109+
* :meth:`~symtable.Symbol.is_comp_iter`
110+
* :meth:`~symtable.Symbol.is_comp_cell`
111+
112+
(Contributed by Bénédikt Tran in :gh:`120029`.)
113+
103114

104115
Optimizations
105116
=============

Include/internal/pycore_symtable.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ extern PyObject* _Py_Mangle(PyObject *p, PyObject *name);
154154
#define DEF_BOUND (DEF_LOCAL | DEF_PARAM | DEF_IMPORT)
155155

156156
/* GLOBAL_EXPLICIT and GLOBAL_IMPLICIT are used internally by the symbol
157-
table. GLOBAL is returned from PyST_GetScope() for either of them.
157+
table. GLOBAL is returned from _PyST_GetScope() for either of them.
158158
It is stored in ste_symbols at bits 13-16.
159159
*/
160160
#define SCOPE_OFFSET 12

Lib/symtable.py

+27-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
from _symtable import (
55
USE,
66
DEF_GLOBAL, DEF_NONLOCAL, DEF_LOCAL,
7-
DEF_PARAM, DEF_TYPE_PARAM, DEF_IMPORT, DEF_BOUND, DEF_ANNOT,
7+
DEF_PARAM, DEF_TYPE_PARAM,
8+
DEF_FREE_CLASS,
9+
DEF_IMPORT, DEF_BOUND, DEF_ANNOT,
10+
DEF_COMP_ITER, DEF_COMP_CELL,
811
SCOPE_OFF, SCOPE_MASK,
912
FREE, LOCAL, GLOBAL_IMPLICIT, GLOBAL_EXPLICIT, CELL
1013
)
@@ -158,6 +161,10 @@ def get_children(self):
158161
for st in self._table.children]
159162

160163

164+
def _get_scope(flags): # like _PyST_GetScope()
165+
return (flags >> SCOPE_OFF) & SCOPE_MASK
166+
167+
161168
class Function(SymbolTable):
162169

163170
# Default values for instance variables
@@ -183,7 +190,7 @@ def get_locals(self):
183190
"""
184191
if self.__locals is None:
185192
locs = (LOCAL, CELL)
186-
test = lambda x: ((x >> SCOPE_OFF) & SCOPE_MASK) in locs
193+
test = lambda x: _get_scope(x) in locs
187194
self.__locals = self.__idents_matching(test)
188195
return self.__locals
189196

@@ -192,7 +199,7 @@ def get_globals(self):
192199
"""
193200
if self.__globals is None:
194201
glob = (GLOBAL_IMPLICIT, GLOBAL_EXPLICIT)
195-
test = lambda x:((x >> SCOPE_OFF) & SCOPE_MASK) in glob
202+
test = lambda x: _get_scope(x) in glob
196203
self.__globals = self.__idents_matching(test)
197204
return self.__globals
198205

@@ -207,7 +214,7 @@ def get_frees(self):
207214
"""Return a tuple of free variables in the function.
208215
"""
209216
if self.__frees is None:
210-
is_free = lambda x:((x >> SCOPE_OFF) & SCOPE_MASK) == FREE
217+
is_free = lambda x: _get_scope(x) == FREE
211218
self.__frees = self.__idents_matching(is_free)
212219
return self.__frees
213220

@@ -234,7 +241,7 @@ class Symbol:
234241
def __init__(self, name, flags, namespaces=None, *, module_scope=False):
235242
self.__name = name
236243
self.__flags = flags
237-
self.__scope = (flags >> SCOPE_OFF) & SCOPE_MASK # like PyST_GetScope()
244+
self.__scope = _get_scope(flags)
238245
self.__namespaces = namespaces or ()
239246
self.__module_scope = module_scope
240247

@@ -303,6 +310,11 @@ def is_free(self):
303310
"""
304311
return bool(self.__scope == FREE)
305312

313+
def is_free_class(self):
314+
"""Return *True* if a class-scoped symbol is free from
315+
the perspective of a method."""
316+
return bool(self.__flags & DEF_FREE_CLASS)
317+
306318
def is_imported(self):
307319
"""Return *True* if the symbol is created from
308320
an import statement.
@@ -313,6 +325,16 @@ def is_assigned(self):
313325
"""Return *True* if a symbol is assigned to."""
314326
return bool(self.__flags & DEF_LOCAL)
315327

328+
def is_comp_iter(self):
329+
"""Return *True* if the symbol is a comprehension iteration variable.
330+
"""
331+
return bool(self.__flags & DEF_COMP_ITER)
332+
333+
def is_comp_cell(self):
334+
"""Return *True* if the symbol is a cell in an inlined comprehension.
335+
"""
336+
return bool(self.__flags & DEF_COMP_CELL)
337+
316338
def is_namespace(self):
317339
"""Returns *True* if name binding introduces new namespace.
318340

Lib/test/test_symtable.py

+21
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,27 @@ def test_symbol_repr(self):
304304
self.assertEqual(repr(self.GenericMine.lookup("T")),
305305
"<symbol 'T': LOCAL, DEF_LOCAL|DEF_TYPE_PARAM>")
306306

307+
st1 = symtable.symtable("[x for x in [1]]", "?", "exec")
308+
self.assertEqual(repr(st1.lookup("x")),
309+
"<symbol 'x': LOCAL, USE|DEF_LOCAL|DEF_COMP_ITER>")
310+
311+
st2 = symtable.symtable("[(lambda: x) for x in [1]]", "?", "exec")
312+
self.assertEqual(repr(st2.lookup("x")),
313+
"<symbol 'x': CELL, DEF_LOCAL|DEF_COMP_ITER|DEF_COMP_CELL>")
314+
315+
st3 = symtable.symtable("def f():\n"
316+
" x = 1\n"
317+
" class A:\n"
318+
" x = 2\n"
319+
" def method():\n"
320+
" return x\n",
321+
"?", "exec")
322+
# child 0 is for __annotate__
323+
func_f = st3.get_children()[1]
324+
class_A = func_f.get_children()[0]
325+
self.assertEqual(repr(class_A.lookup('x')),
326+
"<symbol 'x': LOCAL, DEF_LOCAL|DEF_FREE_CLASS>")
327+
307328
def test_symtable_entry_repr(self):
308329
expected = f"<symtable entry top({self.top.get_id()}), line {self.top.get_lineno()}>"
309330
self.assertEqual(repr(self.top._table), expected)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Expose :class:`symtable.Symbol` methods :meth:`~symtable.Symbol.is_free_class`,
2+
:meth:`~symtable.Symbol.is_comp_iter` and :meth:`~symtable.Symbol.is_comp_cell`.
3+
Patch by Bénédikt Tran.
4+

Modules/symtablemodule.c

+2
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ symtable_init_constants(PyObject *m)
8181
if (PyModule_AddIntMacro(m, DEF_IMPORT) < 0) return -1;
8282
if (PyModule_AddIntMacro(m, DEF_BOUND) < 0) return -1;
8383
if (PyModule_AddIntMacro(m, DEF_ANNOT) < 0) return -1;
84+
if (PyModule_AddIntMacro(m, DEF_COMP_ITER) < 0) return -1;
85+
if (PyModule_AddIntMacro(m, DEF_COMP_CELL) < 0) return -1;
8486

8587
if (PyModule_AddIntConstant(m, "TYPE_FUNCTION", FunctionBlock) < 0)
8688
return -1;

0 commit comments

Comments
 (0)