Skip to content

gh-119698: fix symtable.Class.get_methods and document its behaviour correctly #120151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions Doc/library/symtable.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,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 @@ -228,10 +228,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]:
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 @@ -242,6 +357,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.
Loading