]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-119698: fix a special case in `symtable.Class.get_methods` (#121802)
authorBénédikt Tran <10796600+picnixz@users.noreply.github.com>
Wed, 17 Jul 2024 13:27:35 +0000 (15:27 +0200)
committerGitHub <noreply@github.com>
Wed, 17 Jul 2024 13:27:35 +0000 (06:27 -0700)
Lib/symtable.py
Lib/test/test_symtable.py

index 221aaf88d4167024537348831637265fab637e70..8c796b7cc7edb840fb03b47ca44ee1c3b90c121e 100644 (file)
@@ -249,6 +249,11 @@ class Class(SymbolTable):
                 if is_local_symbol(st.name):
                     match st.type:
                         case _symtable.TYPE_FUNCTION:
+                            # generators are of type TYPE_FUNCTION with a ".0"
+                            # parameter as a first parameter (which makes them
+                            # distinguishable from a function named 'genexpr')
+                            if st.name == 'genexpr' and '.0' in st.varnames:
+                                continue
                             d[st.name] = 1
                         case _symtable.TYPE_TYPE_PARAMETERS:
                             # Get the function-def block in the annotation
@@ -256,7 +261,14 @@ class Class(SymbolTable):
                             scope_name = st.name
                             for c in st.children:
                                 if c.name == scope_name and c.type == _symtable.TYPE_FUNCTION:
-                                    d[st.name] = 1
+                                    # A generic generator of type TYPE_FUNCTION
+                                    # cannot be a direct child of 'st' (but it
+                                    # can be a descendant), e.g.:
+                                    #
+                                    # class A:
+                                    #   type genexpr[genexpr] = (x for x in [])
+                                    assert scope_name != 'genexpr' or '.0' not in c.varnames
+                                    d[scope_name] = 1
                                     break
             self.__methods = tuple(d)
         return self.__methods
index 0cc192655931ba0bc61db345cd94304f04bb17ed..82f667d3687fee5daf86d0632d0acead424828b3 100644 (file)
@@ -1,6 +1,8 @@
 """
 Test the API of the symtable module.
 """
+
+import textwrap
 import symtable
 import unittest
 
@@ -356,7 +358,7 @@ class SymtableTest(unittest.TestCase):
         self.assertEqual(self.spam.lookup("x").get_name(), "x")
         self.assertEqual(self.Mine.get_name(), "Mine")
 
-    def test_class_info(self):
+    def test_class_get_methods(self):
         self.assertEqual(self.Mine.get_methods(), ('a_method',))
 
         top = symtable.symtable(TEST_COMPLEX_CLASS_CODE, "?", "exec")
@@ -377,6 +379,58 @@ class SymtableTest(unittest.TestCase):
             'glob_assigned_async_meth', 'glob_assigned_async_meth_pep_695',
         ))
 
+        # Test generator expressions that are of type TYPE_FUNCTION
+        # but will not be reported by get_methods() since they are
+        # not functions per se.
+        #
+        # Other kind of comprehensions such as list, set or dict
+        # expressions do not have the TYPE_FUNCTION type.
+
+        def check_body(body, expected_methods):
+            indented = textwrap.indent(body, ' ' * 4)
+            top = symtable.symtable(f"class A:\n{indented}", "?", "exec")
+            this = find_block(top, "A")
+            self.assertEqual(this.get_methods(), expected_methods)
+
+        # statements with 'genexpr' inside it
+        GENEXPRS = (
+            'x = (x for x in [])',
+            'x = (x async for x in [])',
+            'type x[genexpr = (x for x in [])] = (x for x in [])',
+            'type x[genexpr = (x async for x in [])] = (x async for x in [])',
+            'genexpr = (x for x in [])',
+            'genexpr = (x async for x in [])',
+            'type genexpr[genexpr = (x for x in [])] = (x for x in [])',
+            'type genexpr[genexpr = (x async for x in [])] = (x async for x in [])',
+        )
+
+        for gen in GENEXPRS:
+            # test generator expression
+            with self.subTest(gen=gen):
+                check_body(gen, ())
+
+            # test generator expression + variable named 'genexpr'
+            with self.subTest(gen=gen, isvar=True):
+                check_body('\n'.join((gen, 'genexpr = 1')), ())
+                check_body('\n'.join(('genexpr = 1', gen)), ())
+
+        for paramlist in ('()', '(x)', '(x, y)', '(z: T)'):
+            for func in (
+                f'def genexpr{paramlist}:pass',
+                f'async def genexpr{paramlist}:pass',
+                f'def genexpr[T]{paramlist}:pass',
+                f'async def genexpr[T]{paramlist}:pass',
+            ):
+                with self.subTest(func=func):
+                    # test function named 'genexpr'
+                    check_body(func, ('genexpr',))
+
+                for gen in GENEXPRS:
+                    with self.subTest(gen=gen, func=func):
+                        # test generator expression + function named 'genexpr'
+                        check_body('\n'.join((gen, func)), ('genexpr',))
+                        check_body('\n'.join((func, gen)), ('genexpr',))
+
     def test_filename_correct(self):
         ### Bug tickler: SyntaxError file name correct whether error raised
         ### while parsing or building symbol table.