Skip to content

Commit 8cfd005

Browse files
[3.13] gh-119698: fix symtable.Class.get_methods and document its behaviour correctly (GH-120151) (#120777)
(cherry picked from commit b8a8e04) Co-authored-by: Bénédikt Tran <[email protected]>
1 parent 5d19490 commit 8cfd005

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
@@ -180,8 +180,39 @@ Examining Symbol Tables
180180

181181
.. method:: get_methods()
182182

183-
Return a tuple containing the names of methods declared in the class.
184-
183+
Return a tuple containing the names of method-like functions declared
184+
in the class.
185+
186+
Here, the term 'method' designates *any* function defined in the class
187+
body via :keyword:`def` or :keyword:`async def`.
188+
189+
Functions defined in a deeper scope (e.g., in an inner class) are not
190+
picked up by :meth:`get_methods`.
191+
192+
For example:
193+
194+
>>> import symtable
195+
>>> st = symtable.symtable('''
196+
... def outer(): pass
197+
...
198+
... class A:
199+
... def f():
200+
... def w(): pass
201+
...
202+
... def g(self): pass
203+
...
204+
... @classmethod
205+
... async def h(cls): pass
206+
...
207+
... global outer
208+
... def outer(self): pass
209+
... ''', 'test', 'exec')
210+
>>> class_A = st.get_children()[1]
211+
>>> class_A.get_methods()
212+
('f', 'g', 'h')
213+
214+
Although ``A().f()`` raises :exc:`TypeError` at runtime, ``A.f`` is still
215+
considered as a method-like function.
185216

186217
.. class:: Symbol
187218

Lib/symtable.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,25 @@ def get_methods(self):
228228
"""
229229
if self.__methods is None:
230230
d = {}
231+
232+
def is_local_symbol(ident):
233+
flags = self._table.symbols.get(ident, 0)
234+
return ((flags >> SCOPE_OFF) & SCOPE_MASK) == LOCAL
235+
231236
for st in self._table.children:
232-
d[st.name] = 1
237+
# pick the function-like symbols that are local identifiers
238+
if is_local_symbol(st.name):
239+
match st.type:
240+
case _symtable.TYPE_FUNCTION:
241+
d[st.name] = 1
242+
case _symtable.TYPE_TYPE_PARAMETERS:
243+
# Get the function-def block in the annotation
244+
# scope 'st' with the same identifier, if any.
245+
scope_name = st.name
246+
for c in st.children:
247+
if c.name == scope_name and c.type == _symtable.TYPE_FUNCTION:
248+
d[st.name] = 1
249+
break
233250
self.__methods = tuple(d)
234251
return self.__methods
235252

Lib/test/test_symtable.py

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
1414
glob = 42
1515
some_var = 12
16-
some_non_assigned_global_var = 11
16+
some_non_assigned_global_var: int
1717
some_assigned_global_var = 11
1818
1919
class Mine:
@@ -53,6 +53,120 @@ class GenericMine[T: int, U: (int, str) = int]:
5353
pass
5454
"""
5555

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

57171
def find_block(block, name):
58172
for ch in block.get_children():
@@ -65,6 +179,7 @@ class SymtableTest(unittest.TestCase):
65179
top = symtable.symtable(TEST_CODE, "?", "exec")
66180
# These correspond to scopes in TEST_CODE
67181
Mine = find_block(top, "Mine")
182+
68183
a_method = find_block(Mine, "a_method")
69184
spam = find_block(top, "spam")
70185
internal = find_block(spam, "internal")
@@ -242,6 +357,24 @@ def test_name(self):
242357
def test_class_info(self):
243358
self.assertEqual(self.Mine.get_methods(), ('a_method',))
244359

360+
top = symtable.symtable(TEST_COMPLEX_CLASS_CODE, "?", "exec")
361+
this = find_block(top, "ComplexClass")
362+
363+
self.assertEqual(this.get_methods(), (
364+
'a_method', 'a_method_pep_695',
365+
'an_async_method', 'an_async_method_pep_695',
366+
'a_classmethod', 'a_classmethod_pep_695',
367+
'an_async_classmethod', 'an_async_classmethod_pep_695',
368+
'a_staticmethod', 'a_staticmethod_pep_695',
369+
'an_async_staticmethod', 'an_async_staticmethod_pep_695',
370+
'a_fakemethod', 'a_fakemethod_pep_695',
371+
'an_async_fakemethod', 'an_async_fakemethod_pep_695',
372+
'glob_unassigned_meth', 'glob_unassigned_meth_pep_695',
373+
'glob_unassigned_async_meth', 'glob_unassigned_async_meth_pep_695',
374+
'glob_assigned_meth', 'glob_assigned_meth_pep_695',
375+
'glob_assigned_async_meth', 'glob_assigned_async_meth_pep_695',
376+
))
377+
245378
def test_filename_correct(self):
246379
### Bug tickler: SyntaxError file name correct whether error raised
247380
### 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)