Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
16 changes: 14 additions & 2 deletions Doc/library/symtable.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,20 @@ 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.

Note that the term 'method' here designates *any* function directly
declared via :keyword:`def` inside the class body. For instance::

>>> import symtable
>>> st = symtable.symtable("class A:\n"
... " def f(): pass\n"
... " def g(self): pass\n",
... "test", "exec")
>>> class_A = st.get_children()[0]
>>> class_A.get_methods()
('f', 'g')

.. class:: Symbol

Expand Down
21 changes: 20 additions & 1 deletion Lib/symtable.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,27 @@ def get_methods(self):
"""
if self.__methods is None:
d = {}
Comment thread
picnixz marked this conversation as resolved.

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:
d[st.name] = 1
# pick the function-like symbols that are local identifiers
if is_local_symbol(st.name):
if st.type == _symtable.TYPE_TYPE_PARAM:
# Current 'st' is an annotation scope with one or
# more children (we expect only one, but we might
# have more in the future). In particular, we need
# to find the corresponding inner function, class or
# type alias.
st = next((c for c in st.children if c.name == st.name), None)
Comment thread
picnixz marked this conversation as resolved.
Outdated
# if 'st' is None, then the annotation scopes are broken
assert st is not None, 'annotation scopes are broken'

# only select function-like symbols
if st.type == _symtable.TYPE_FUNCTION:
d[st.name] = 1
self.__methods = tuple(d)
return self.__methods

Expand Down
128 changes: 127 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 @@ -51,6 +51,115 @@ def generic_spam[T](a):

class GenericMine[T: int]:
pass

# The following symbols are defined in ComplexClass
# without being introduced by a 'global' statement.
glob_unassigned_meth: int
glob_unassigned_meth_pep_695: int
glob_unassigned_async_meth: int
glob_unassigned_async_meth_pep_695: int

glob_assigned_meth = 1234
glob_assigned_meth_pep_695 = 1234
glob_assigned_async_meth = 1234
glob_assigned_async_meth_pep_695 = 1234

# The following symbols are defined in ComplexClass after
# being introduced by a 'global' statement (and therefore
# are not considered as methods of ComplexClass).
glob_unassigned_meth_ignore: int
glob_unassigned_meth_pep_695_ignore: int
glob_unassigned_async_meth_ignore: int
glob_unassigned_async_meth_pep_695_ignore: int

glob_assigned_meth_ignore = 1234
glob_assigned_meth_pep_695_ignore = 1234
glob_assigned_async_meth_ignore = 1234
glob_assigned_async_meth_pep_695_ignore = 1234

class ComplexClass:
some_non_method_const = 1234

class some_non_method_nested: pass
class some_non_method_nested_pep_695[T]: pass

type some_non_method_alias = int
type some_non_method_alias_pep_695[T] = list[T]

some_non_method_genexpr = (x for x in [])
some_non_method_lambda = lambda x: x

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
def an_async_staticmethod(): pass
@staticmethod
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 a method because thy 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
Comment thread
picnixz marked this conversation as resolved.

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
"""


Expand All @@ -65,6 +174,8 @@ class SymtableTest(unittest.TestCase):
top = symtable.symtable(TEST_CODE, "?", "exec")
# These correspond to scopes in TEST_CODE
Mine = find_block(top, "Mine")
ComplexClass = find_block(top, "ComplexClass")

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

self.assertEqual(self.ComplexClass.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.