Skip to content

Commit

Permalink
pythongh-119698: fix symtable.Class.get_methods and document its be…
Browse files Browse the repository at this point in the history
…haviour correctly (python#120151)



Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
  • Loading branch information
2 people authored and noahbkim committed Jul 11, 2024
1 parent 353d90d commit 9b8c175
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 6 deletions.
35 changes: 33 additions & 2 deletions Doc/library/symtable.rst
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,39 @@ Examining Symbol Tables

.. method:: get_methods()

Return a tuple containing the names of methods declared in the class.

Return a tuple containing the names of method-like functions declared
in the class.

Here, the term 'method' designates *any* function defined in the class
body via :keyword:`def` or :keyword:`async def`.

Functions defined in a deeper scope (e.g., in an inner class) are not
picked up by :meth:`get_methods`.

For example:

>>> import symtable
>>> st = symtable.symtable('''
... def outer(): pass
...
... class A:
... def f():
... def w(): pass
...
... def g(self): pass
...
... @classmethod
... async def h(cls): pass
...
... global outer
... def outer(self): pass
... ''', 'test', 'exec')
>>> class_A = st.get_children()[2]
>>> class_A.get_methods()
('f', 'g', 'h')

Although ``A().f()`` raises :exc:`TypeError` at runtime, ``A.f`` is still
considered as a method-like function.

.. class:: Symbol

Expand Down
21 changes: 18 additions & 3 deletions Lib/symtable.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,25 @@ def get_methods(self):
"""
if self.__methods is None:
d = {}

def is_local_symbol(ident):
flags = self._table.symbols.get(ident, 0)
return ((flags >> SCOPE_OFF) & SCOPE_MASK) == LOCAL

for st in self._table.children:
if st.type == _symtable.TYPE_ANNOTATION:
continue
d[st.name] = 1
# pick the function-like symbols that are local identifiers
if is_local_symbol(st.name):
match st.type:
case _symtable.TYPE_FUNCTION:
d[st.name] = 1
case _symtable.TYPE_TYPE_PARAM:
# Get the function-def block in the annotation
# scope 'st' with the same identifier, if any.
scope_name = st.name
for c in st.children:
if c.name == scope_name and c.type == _symtable.TYPE_FUNCTION:
d[st.name] = 1
break
self.__methods = tuple(d)
return self.__methods

Expand Down
135 changes: 134 additions & 1 deletion Lib/test/test_symtable.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
glob = 42
some_var = 12
some_non_assigned_global_var = 11
some_non_assigned_global_var: int
some_assigned_global_var = 11
class Mine:
Expand Down Expand Up @@ -53,6 +53,120 @@ class GenericMine[T: int, U: (int, str) = int]:
pass
"""

TEST_COMPLEX_CLASS_CODE = """
# The following symbols are defined in ComplexClass
# without being introduced by a 'global' statement.
glob_unassigned_meth: Any
glob_unassigned_meth_pep_695: Any
glob_unassigned_async_meth: Any
glob_unassigned_async_meth_pep_695: Any
def glob_assigned_meth(): pass
def glob_assigned_meth_pep_695[T](): pass
async def glob_assigned_async_meth(): pass
async def glob_assigned_async_meth_pep_695[T](): pass
# The following symbols are defined in ComplexClass after
# being introduced by a 'global' statement (and therefore
# are not considered as local symbols of ComplexClass).
glob_unassigned_meth_ignore: Any
glob_unassigned_meth_pep_695_ignore: Any
glob_unassigned_async_meth_ignore: Any
glob_unassigned_async_meth_pep_695_ignore: Any
def glob_assigned_meth_ignore(): pass
def glob_assigned_meth_pep_695_ignore[T](): pass
async def glob_assigned_async_meth_ignore(): pass
async def glob_assigned_async_meth_pep_695_ignore[T](): pass
class ComplexClass:
a_var = 1234
a_genexpr = (x for x in [])
a_lambda = lambda x: x
type a_type_alias = int
type a_type_alias_pep_695[T] = list[T]
class a_class: pass
class a_class_pep_695[T]: pass
def a_method(self): pass
def a_method_pep_695[T](self): pass
async def an_async_method(self): pass
async def an_async_method_pep_695[T](self): pass
@classmethod
def a_classmethod(cls): pass
@classmethod
def a_classmethod_pep_695[T](self): pass
@classmethod
async def an_async_classmethod(cls): pass
@classmethod
async def an_async_classmethod_pep_695[T](self): pass
@staticmethod
def a_staticmethod(): pass
@staticmethod
def a_staticmethod_pep_695[T](self): pass
@staticmethod
async def an_async_staticmethod(): pass
@staticmethod
async def an_async_staticmethod_pep_695[T](self): pass
# These ones will be considered as methods because of the 'def' although
# they are *not* valid methods at runtime since they are not decorated
# with @staticmethod.
def a_fakemethod(): pass
def a_fakemethod_pep_695[T](): pass
async def an_async_fakemethod(): pass
async def an_async_fakemethod_pep_695[T](): pass
# Check that those are still considered as methods
# since they are not using the 'global' keyword.
def glob_unassigned_meth(): pass
def glob_unassigned_meth_pep_695[T](): pass
async def glob_unassigned_async_meth(): pass
async def glob_unassigned_async_meth_pep_695[T](): pass
def glob_assigned_meth(): pass
def glob_assigned_meth_pep_695[T](): pass
async def glob_assigned_async_meth(): pass
async def glob_assigned_async_meth_pep_695[T](): pass
# The following are not picked as local symbols because they are not
# visible by the class at runtime (this is equivalent to having the
# definitions outside of the class).
global glob_unassigned_meth_ignore
def glob_unassigned_meth_ignore(): pass
global glob_unassigned_meth_pep_695_ignore
def glob_unassigned_meth_pep_695_ignore[T](): pass
global glob_unassigned_async_meth_ignore
async def glob_unassigned_async_meth_ignore(): pass
global glob_unassigned_async_meth_pep_695_ignore
async def glob_unassigned_async_meth_pep_695_ignore[T](): pass
global glob_assigned_meth_ignore
def glob_assigned_meth_ignore(): pass
global glob_assigned_meth_pep_695_ignore
def glob_assigned_meth_pep_695_ignore[T](): pass
global glob_assigned_async_meth_ignore
async def glob_assigned_async_meth_ignore(): pass
global glob_assigned_async_meth_pep_695_ignore
async def glob_assigned_async_meth_pep_695_ignore[T](): pass
"""


def find_block(block, name):
for ch in block.get_children():
Expand All @@ -65,6 +179,7 @@ class SymtableTest(unittest.TestCase):
top = symtable.symtable(TEST_CODE, "?", "exec")
# These correspond to scopes in TEST_CODE
Mine = find_block(top, "Mine")

a_method = find_block(Mine, "a_method")
spam = find_block(top, "spam")
internal = find_block(spam, "internal")
Expand Down Expand Up @@ -244,6 +359,24 @@ def test_name(self):
def test_class_info(self):
self.assertEqual(self.Mine.get_methods(), ('a_method',))

top = symtable.symtable(TEST_COMPLEX_CLASS_CODE, "?", "exec")
this = find_block(top, "ComplexClass")

self.assertEqual(this.get_methods(), (
'a_method', 'a_method_pep_695',
'an_async_method', 'an_async_method_pep_695',
'a_classmethod', 'a_classmethod_pep_695',
'an_async_classmethod', 'an_async_classmethod_pep_695',
'a_staticmethod', 'a_staticmethod_pep_695',
'an_async_staticmethod', 'an_async_staticmethod_pep_695',
'a_fakemethod', 'a_fakemethod_pep_695',
'an_async_fakemethod', 'an_async_fakemethod_pep_695',
'glob_unassigned_meth', 'glob_unassigned_meth_pep_695',
'glob_unassigned_async_meth', 'glob_unassigned_async_meth_pep_695',
'glob_assigned_meth', 'glob_assigned_meth_pep_695',
'glob_assigned_async_meth', 'glob_assigned_async_meth_pep_695',
))

def test_filename_correct(self):
### Bug tickler: SyntaxError file name correct whether error raised
### while parsing or building symbol table.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix :meth:`symtable.Class.get_methods` and document its behaviour. Patch by
Bénédikt Tran.

0 comments on commit 9b8c175

Please sign in to comment.