summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJelle Zijlstra <jelle.zijlstra@gmail.com>2024-06-20 05:27:23 (GMT)
committerGitHub <noreply@github.com>2024-06-20 05:27:23 (GMT)
commit8cfd005b6df2f1d07c0dd00450009a41796a2718 (patch)
tree67fce9c8dae1671ac8705e7d4089c9d28018d8be
parent5d194902cbe9eff0a4f974c65479046c251664fd (diff)
downloadcpython-8cfd005b6df2f1d07c0dd00450009a41796a2718.zip
cpython-8cfd005b6df2f1d07c0dd00450009a41796a2718.tar.gz
cpython-8cfd005b6df2f1d07c0dd00450009a41796a2718.tar.bz2
[3.13] gh-119698: fix `symtable.Class.get_methods` and document its behaviour correctly (GH-120151) (#120777)
(cherry picked from commit b8a8e04fec76ad7f7c3e5149114dd2ee8a5caecc) Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
-rw-r--r--Doc/library/symtable.rst35
-rw-r--r--Lib/symtable.py19
-rw-r--r--Lib/test/test_symtable.py135
-rw-r--r--Misc/NEWS.d/next/Library/2024-06-06-12-07-57.gh-issue-119698.rRrprk.rst2
4 files changed, 187 insertions, 4 deletions
diff --git a/Doc/library/symtable.rst b/Doc/library/symtable.rst
index cc10265..ca7eaef 100644
--- a/Doc/library/symtable.rst
+++ b/Doc/library/symtable.rst
@@ -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()[1]
+ >>> 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
diff --git a/Lib/symtable.py b/Lib/symtable.py
index 500a990..73e9fb3 100644
--- a/Lib/symtable.py
+++ b/Lib/symtable.py
@@ -228,8 +228,25 @@ class Class(SymbolTable):
"""
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:
- 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_PARAMETERS:
+ # 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
diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py
index ebd6094..2443898 100644
--- a/Lib/test/test_symtable.py
+++ b/Lib/test/test_symtable.py
@@ -13,7 +13,7 @@ import sys
glob = 42
some_var = 12
-some_non_assigned_global_var = 11
+some_non_assigned_global_var: int
some_assigned_global_var = 11
class Mine:
@@ -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():
@@ -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")
@@ -242,6 +357,24 @@ class SymtableTest(unittest.TestCase):
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.
diff --git a/Misc/NEWS.d/next/Library/2024-06-06-12-07-57.gh-issue-119698.rRrprk.rst b/Misc/NEWS.d/next/Library/2024-06-06-12-07-57.gh-issue-119698.rRrprk.rst
new file mode 100644
index 0000000..d4cca14
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-06-06-12-07-57.gh-issue-119698.rRrprk.rst
@@ -0,0 +1,2 @@
+Fix :meth:`symtable.Class.get_methods` and document its behaviour. Patch by
+Bénédikt Tran.