8000 [3.12] gh-119698: fix `symtable.Class.get_methods` and document its b… · python/cpython@0c6d6ab · GitHub
[go: up one dir, main page]

Skip to content

Commit 0c6d6ab

Browse files
[3.12] gh-119698: fix symtable.Class.get_methods and document its behaviour correctly (#120151) (#120776)
(cherry picked from commit b8a8e04) Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
1 parent 61e37dd commit 0c6d6ab

File tree

4 files changed

+187
-4
lines changed

4 files changed

+187
-4
lines changed

Doc/library/symtable.rst

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,39 @@ Examining Symbol Tables
127127

128128
.. method:: get_methods()
129129

130-
Return a tuple containing the names of methods declared in the class.
131-
130+
Return a tuple containing the names of method-like functions declared
131+
in the class.
132+
133+
Here, the term 'method' designates *any* function defined in the class
134+
body via :keyword:`def` or :keyword:`async def`.
135+
136+
Functions defined in a deeper scope (e.g., in an inner class) are not
137+
picked up by :meth:`get_methods`.
138+
139+
For example:
140+
141+
>>> import symtable
142+
>>> st = symtable.symtable('''
143+
... def outer(): pass
144+
...
145+
... class A:
146+
... def f():
147+
... def w(): pass
148+
...
149+
... def g(self): pass
150+
...
151+
... @classmethod
152+
... async def h(cls): pass
153+
...
154+
... global outer
155+
... def outer(self): pass
156+
... ''', 'test', 'exec')
157+
>>> class_A = st.get_children()[1]
158+
>>> class_A.get_methods()
159+
('f', 'g', 'h')
160+
161+
Although ``A().f()`` raises :exc:`TypeError` at runtime, ``A.f`` is still
162+
considered as a method-like function.
132163

133164
.. class:: Symbol
134165

Lib/symtable.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,25 @@ def get_methods(self):
217217
"""
218218
if self.__methods is None:
219219
d = {}
220+
221+
def is_local_symbol(ident):
222+
flags = self._table.symbols.get(ident, 0)
223+
return ((flags >> SCOPE_OFF) & SCOPE_MASK) == LOCAL
224+
220225
for st in self._table.children:
221-
d[st.name] = 1
226+
# pick the function-like symbols that are local identifiers
227+
if is_local_symbol(st.name):
228+
match st.type:
229+
case _symtable.TYPE_FUNCTION:
230+
d[st.name] = 1
231+
case _symtable.TYPE_TYPE_PARAM:
232+
# Get the function-def block in the annotation
233+
# scope 'st' with the same identifier, if any.
234+
scope_name = st.name
235+
for c in st.children:
236+
if c.name == scope_name and c.type == _symtable.TYPE_FUNCTION:
237+
d[st.name] = 1
238+
break
222239
self.__methods = tuple(d)
223240
return self.__methods
224241

Lib/test/test_symtable.py

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
1212
glob = 42
1313
some_var = 12
14-
some_non_assigned_global_var = 11
14+
some_non_assigned_global_var: int
1515
some_assigned_global_var = 11
1616
1717
class Mine:
@@ -51,6 +51,120 @@ class GenericMine[T: int]:
5151
pass
5252
"""
5353

54+
TEST_COMPLEX_CLASS_CODE = """
55+
# The following symbols are defined in ComplexClass
56+
# without being introduced by a 'global' statement.
57+
glob_unassigned_meth: Any
58+
glob_unassigned_meth_pep_695: Any
59+
60+
glob_unassigned_async_meth: Any
61+
glob_unassigned_async_meth_pep_695: Any
62+
63+
def glob_assigned_meth(): pass
64+
def glob_assigned_meth_pep_695[T](): pass
65+
66+
async def glob_assigned_async_meth(): pass
67+
async def glob_assigned_async_meth_pep_695[T](): pass
68+
69+
# The following symbols are defined in ComplexClass after
70+
# being introduced by a 'global' statement (and therefore
71+
# are not considered as local symbols of ComplexClass).
72+
glob_unassigned_meth_ignore: Any
73+
glob_unassigned_meth_pep_695_ignore: Any
74+
75+
glob_unassigned_async_meth_ignore: Any
76+
glob_unassigned_async_meth_pep_695_ignore: Any
77+
78+
def glob_assigned_meth_ignore(): pass
79+
def glob_assigned_meth_pep_695_ignore[T](): pass
80+
81+
async def glob_assigned_async_meth_ignore(): pass
82+
async def glob_assigned_async_meth_pep_695_ignore[T](): pass
83+
84+
class ComplexClass:
85+
a_var = 1234
86+
a_genexpr = (x for x in [])
87+
a_lambda = lambda x: x
88+
89+
type a_type_alias = int
90+
type a_type_alias_pep_695[T] = list[T]
91+
92+
class a_class: pass
93+
class a_class_pep_695[T]: pass
94+
95+
def a_method(self): pass
96+
def a_method_pep_695[T](self): pass
97+
98+
async def an_async_method(self): pass
99+
async def an_async_method_pep_695[T](self): pass
100+
101+
@classmethod
102+
def a_classmethod(cls): pass
103+
@classmethod
104+
def a_classmethod_pep_695[T](self): pass
105+
106+
@classmethod
107+
async def an_async_classmethod(cls): pass
108+
@classmethod
109+
async def an_async_classmethod_pep_695[T](self): pass
110+
111+
@staticmethod
112+
def a_staticmethod(): pass
113+
@staticmethod
114+
def a_staticmethod_pep_695[T](self): pass
115+
116+
@staticmethod
117+
async def an_async_staticmethod(): pass
118+
@staticmethod
119+
async def an_async_staticmethod_pep_695[T](self): pass
120+
121+
# These ones will be considered as methods because of the 'def' although
122+
# they are *not* valid methods at runtime since they are not decorated
123+
# with @staticmethod.
124+
def a_fakemethod(): pass
125+
def a_fakemethod_pep_695[T](): pass
126+
127+
async def an_async_fakemethod(): pass
128+
async def an_async_fakemethod_pep_695[T](): pass
129+
130+
# Check that those are still considered as methods
131+
# since they are not using the 'global' keyword.
132+
def glob_unassigned_meth(): pass
133+
def glob_unassigned_meth_pep_695[T](): pass
134+
135+
async def glob_unassigned_async_meth(): pass
136+
async def glob_unassigned_async_meth_pep_695[T](): pass
137+
138+
def glob_assigned_meth(): pass
139+
def glob_assigned_meth_pep_695[T](): pass
140+
141+
async def glob_assigned_async_meth(): pass
142+
async def glob_assigned_async_meth_pep_695[T](): pass
143+
144+
# The following are not picked as local symbols because they are not
145+
# visible by the class at runtime (this is equivalent to having the
146+
# definitions outside of the class).
147+
global glob_unassigned_meth_ignore
148+
def glob_unassigned_meth_ignore(): pass
149+
global glob_unassigned_meth_pep_695_ignore
150+
def glob_unassigned_meth_pep_695_ignore[T](): pass
151+
152+
global glob_unassigned_async_meth_ignore
153+
async def glob_unassigned_async_meth_ignore(): pass
154+
global glob_unassigned_async_meth_pep_695_ignore
155+
async def glob_unassigned_async_meth_pep_695_ignore[T](): pass
156+
157+
global glob_assigned_meth_ignore
158+
def glob_assigned_meth_ignore(): pass
159+
global glob_assigned_meth_pep_695_ignore
160+
def glob_assigned_meth_pep_695_ignore[T](): pass
161+
162+
global glob_assigned_async_meth_ignore
163+
async def glob_assigned_async_meth_ignore(): pass
164+
global glob_assigned_async_meth_pep_695_ignore
165+
async def glob_assigned_async_meth_pep_695_ignore[T](): pass
166+
"""
167+
54168

55169
def find_block(block, name):
56170
for ch in block.get_children():
@@ -63,6 +177,7 @@ class SymtableTest(unittest.TestCase):
63177
top = symtable.symtable(TEST_CODE, "?", "exec")
64178
# These correspond to scopes in TEST_CODE
65179
Mine = find_block(top, "Mine")
180+
66181
a_method = find_block(Mine, "a_method")
67182
spam = find_block(top, "spam")
68183
internal = find_block(spam, "internal")
238353
def test_class_info(self):
239354
self.assertEqual(self.Mine.get_methods(), ('a_method',))
240355

356+
top = symtable.symtable(TEST_COMPLEX_CLASS_CODE, "?", "exec")
357+
this = find_block(top, "ComplexClass")
358+
359+
self.assertEqual(this.get_methods(), (
360+
'a_method', 'a_method_pep_695',
361+
'an_async_method', 'an_async_method_pep_695',
362+
'a_classmethod', 'a_classmethod_pep_695',
363+
'an_async_classmethod', 'an_async_classmethod_pep_695',
364+
'a_staticmethod', 'a_staticmethod_pep_695',
365+
'an_async_staticmethod', 'an_async_staticmethod_pep_695',
366+
'a_fakemethod', 'a_fakemethod_pep_695',
367+
'an_async_fakemethod', 'an_async_fakemethod_pep_695',
368+
'glob_unassigned_meth', 'glob_unassigned_meth_pep_695',
369+
'glob_unassigned_async_meth', 'glob_unassigned_async_meth_pep_695',
370+
'glob_assigned_meth', 'glob_assigned_meth_pep_695',
371+
'glob_assigned_async_meth', 'glob_assigned_async_meth_pep_695',
372+
))
373+
241374
def test_filename_correct(self):
242375
### Bug tickler: SyntaxError file name correct whether error raised
243376
### while parsing or building symbol table.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :meth:`symtable.Class.get_methods` and document its behaviour. Patch by
2+
Bénédikt Tran.

0 commit comments

Comments
 (0)
0