]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-119698: fix `symtable.Class.get_methods` and document its behaviour correctly...
authorBénédikt Tran <10796600+picnixz@users.noreply.github.com>
Thu, 20 Jun 2024 03:49:30 +0000 (05:49 +0200)
committerGitHub <noreply@github.com>
Thu, 20 Jun 2024 03:49:30 +0000 (20:49 -0700)
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
Doc/library/symtable.rst
Lib/symtable.py
Lib/test/test_symtable.py
Misc/NEWS.d/next/Library/2024-06-06-12-07-57.gh-issue-119698.rRrprk.rst [new file with mode: 0644]

index a33263796ee53d046fd96b613610305dc5ecac8f..aa5f8d95925adaf7a066d6bed77159d389329710 100644 (file)
@@ -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
 
index 2522cf44de20bd5034474d630050fac3699a81ce..fee3c43163f6c22b0daa5275fc1e8fc8beef0904 100644 (file)
@@ -239,10 +239,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:
-                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
 
index 175f453447bab9f98a0c1bc7b984628c52cc846e..0cc192655931ba0bc61db345cd94304f04bb17ed 100644 (file)
@@ -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")
@@ -244,6 +359,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 (file)
index 0000000..d4cca14
--- /dev/null
@@ -0,0 +1,2 @@
+Fix :meth:`symtable.Class.get_methods` and document its behaviour. Patch by
+Bénédikt Tran.