From 5b2389241d40137f64ed53ebeac58ebaf9a1f9f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:00:31 +0200 Subject: [PATCH 01/19] fix `symtable.Class.get_methods` --- Lib/symtable.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Lib/symtable.py b/Lib/symtable.py index ba2f0dafcd0063..55c64ff4334f07 100644 --- a/Lib/symtable.py +++ b/Lib/symtable.py @@ -221,8 +221,15 @@ 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: - d[st.name] = 1 + # only pick the 'function' symbols that are local identifiers + if st.type == _symtable.TYPE_FUNCTION and is_local_symbol(st.name): + d[st.name] = 1 self.__methods = tuple(d) return self.__methods From 93ea543b38514ae0daa641192d19c6893a033aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Thu, 6 Jun 2024 11:25:58 +0200 Subject: [PATCH 02/19] add test --- Lib/test/test_symtable.py | 47 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py index ef2a228b15ed4e..a88c942dc62ce7 100644 --- a/Lib/test/test_symtable.py +++ b/Lib/test/test_symtable.py @@ -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: @@ -51,6 +51,40 @@ def generic_spam[T](a): class GenericMine[T: int]: pass + +some_non_assigned_global_ident: int +some_non_assigned_global_ident_2: int +some_assigned_global_ident = None + +class ComplexClass: + some_non_method_const = 1234 + + class some_non_method_nested: pass + type some_non_method_alias = int + + some_non_method_genexpr = (x for x in []) + some_non_method_lambda = lambda x: x + + def a_method(self): pass + @classmethod + def a_classmethod(cls): pass + @staticmethod + def a_staticmethod(): pass + + # this one will be considered as a method because of the 'def' although + # it will *not* be a valid one at runtime since it is not a staticmethod. + def a_fakemethod(): pass + + # check that those are still considered as methods + # since they are not using the 'global' keyword + def some_assigned_global_ident(): pass + def some_non_assigned_global_ident(): pass + + # this one is not picked as a method because it will not even be + # visible by the class at runtime (this is equivalent to having + # that definition outside of the class). + global some_non_assigned_global_ident_2 + def some_non_assigned_global_ident_2(): pass """ @@ -65,6 +99,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") @@ -240,6 +276,15 @@ 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_classmethod', + 'a_staticmethod', + 'a_fakemethod', + 'some_assigned_global_ident', + 'some_non_assigned_global_ident', + )) + def test_filename_correct(self): ### Bug tickler: SyntaxError file name correct whether error raised ### while parsing or building symbol table. From 44f50e2762aece4174fecf5da4c9c9b568d732dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:01:34 +0200 Subject: [PATCH 03/19] add doc --- Doc/library/symtable.rst | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Doc/library/symtable.rst b/Doc/library/symtable.rst index e17a33f7feb1ab..27da47150e3d69 100644 --- a/Doc/library/symtable.rst +++ b/Doc/library/symtable.rst @@ -127,8 +127,19 @@ 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:: + + >>> 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 From f8b95d33eff1830e3eb155f748788a2a46264aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:08:00 +0200 Subject: [PATCH 04/19] blurb --- .../next/Library/2024-06-06-12-07-57.gh-issue-119698.rRrprk.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-06-06-12-07-57.gh-issue-119698.rRrprk.rst 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 00000000000000..d4cca1439816b0 --- /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. From 582906bf1fbad1417c034ade46d6fc3f3f83c5ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:10:05 +0200 Subject: [PATCH 05/19] fixup --- Lib/test/test_symtable.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py index a88c942dc62ce7..73a08f3efa2fa5 100644 --- a/Lib/test/test_symtable.py +++ b/Lib/test/test_symtable.py @@ -58,28 +58,28 @@ class GenericMine[T: int]: class ComplexClass: some_non_method_const = 1234 - + class some_non_method_nested: pass type some_non_method_alias = int - + some_non_method_genexpr = (x for x in []) some_non_method_lambda = lambda x: x - + def a_method(self): pass @classmethod def a_classmethod(cls): pass @staticmethod def a_staticmethod(): pass - + # this one will be considered as a method because of the 'def' although # it will *not* be a valid one at runtime since it is not a staticmethod. def a_fakemethod(): pass - + # check that those are still considered as methods # since they are not using the 'global' keyword def some_assigned_global_ident(): pass def some_non_assigned_global_ident(): pass - + # this one is not picked as a method because it will not even be # visible by the class at runtime (this is equivalent to having # that definition outside of the class). From f64e302d1466cb0655b5a53cd599488c1d9e774b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:12:51 +0200 Subject: [PATCH 06/19] fixup whitespaces --- Lib/test/test_symtable.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py index 73a08f3efa2fa5..114dc1e493ed51 100644 --- a/Lib/test/test_symtable.py +++ b/Lib/test/test_symtable.py @@ -63,7 +63,7 @@ class some_non_method_nested: pass type some_non_method_alias = int some_non_method_genexpr = (x for x in []) - some_non_method_lambda = lambda x: x + some_non_method_lambda = lambda x: x def a_method(self): pass @classmethod @@ -71,16 +71,16 @@ def a_classmethod(cls): pass @staticmethod def a_staticmethod(): pass - # this one will be considered as a method because of the 'def' although + # This one will be considered as a method because of the 'def' although # it will *not* be a valid one at runtime since it is not a staticmethod. def a_fakemethod(): pass - # check that those are still considered as methods - # since they are not using the 'global' keyword + # Check that those are still considered as methods + # since they are not using the 'global' keyword. def some_assigned_global_ident(): pass def some_non_assigned_global_ident(): pass - # this one is not picked as a method because it will not even be + # This one is not picked as a method because it will not even be # visible by the class at runtime (this is equivalent to having # that definition outside of the class). global some_non_assigned_global_ident_2 From 6078d24e2e37a8fab07cbb282be3932d40da02b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:21:31 +0200 Subject: [PATCH 07/19] fixup --- Doc/library/symtable.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/library/symtable.rst b/Doc/library/symtable.rst index 27da47150e3d69..84cf170643758a 100644 --- a/Doc/library/symtable.rst +++ b/Doc/library/symtable.rst @@ -131,8 +131,9 @@ Examining Symbol Tables in the class. Note that the term 'method' here designates *any* function directly - declared via :keyword:`def` inside the class body. For instance:: + 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", From f795499f5ee200292e276642601944ca6aab5369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:28:19 +0200 Subject: [PATCH 08/19] fixup --- Doc/library/symtable.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Doc/library/symtable.rst b/Doc/library/symtable.rst index 84cf170643758a..c1fd1c107ac63b 100644 --- a/Doc/library/symtable.rst +++ b/Doc/library/symtable.rst @@ -131,16 +131,16 @@ Examining Symbol Tables 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') + 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 From 8d1f661e5a1b634ca7970124e337ee2893d8f2df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:06:13 +0200 Subject: [PATCH 09/19] improve test coverage --- Lib/symtable.py | 16 ++++++++-- Lib/test/test_symtable.py | 61 ++++++++++++++++++++++++++++----------- 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/Lib/symtable.py b/Lib/symtable.py index 55c64ff4334f07..07860fd57247de 100644 --- a/Lib/symtable.py +++ b/Lib/symtable.py @@ -227,9 +227,19 @@ def is_local_symbol(ident): return ((flags >> SCOPE_OFF) & SCOPE_MASK) == LOCAL for st in self._table.children: - # only pick the 'function' symbols that are local identifiers - if st.type == _symtable.TYPE_FUNCTION and is_local_symbol(st.name): - 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 , + # so 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) + # if 'st' is None, then the annotation scopes are broken + assert st is not None, 'annotation scopes are broken' + + if st.type == _symtable.TYPE_FUNCTION: + d[st.name] = 1 self.__methods = tuple(d) return self.__methods diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py index 114dc1e493ed51..f93385f167cea0 100644 --- a/Lib/test/test_symtable.py +++ b/Lib/test/test_symtable.py @@ -52,39 +52,66 @@ def generic_spam[T](a): class GenericMine[T: int]: pass -some_non_assigned_global_ident: int -some_non_assigned_global_ident_2: int -some_assigned_global_ident = None +# test case: ComplexClass +glob_unassigned_meth: int +glob_unassigned_meth_pep_695: int +glob_assigned_meth = 1234 +glob_assigned_meth_pep_695 = 1234 + +glob_unassigned_meth_ignore: int +glob_unassigned_meth_pep_695_ignore: int +glob_assigned_meth_ignore = 1234 +glob_assigned_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 + @classmethod def a_classmethod(cls): pass + @classmethod + def a_classmethod_pep_695[T](self): pass + @staticmethod def a_staticmethod(): pass + @staticmethod + def a_staticmethod_pep_695[T](self): pass - # This one will be considered as a method because of the 'def' although - # it will *not* be a valid one at runtime since it is not a staticmethod. + # 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 # Check that those are still considered as methods # since they are not using the 'global' keyword. - def some_assigned_global_ident(): pass - def some_non_assigned_global_ident(): pass + def glob_unassigned_meth(): pass + def glob_unassigned_meth_pep_695[T](): pass + def glob_assigned_meth(): pass + def glob_assigned_meth_pep_695[T](): pass - # This one is not picked as a method because it will not even be + # The following are not picked as a method because thy are not # visible by the class at runtime (this is equivalent to having - # that definition outside of the class). - global some_non_assigned_global_ident_2 - def some_non_assigned_global_ident_2(): pass + # 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_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 """ @@ -277,12 +304,12 @@ def test_class_info(self): self.assertEqual(self.Mine.get_methods(), ('a_method',)) self.assertEqual(self.ComplexClass.get_methods(), ( - 'a_method', - 'a_classmethod', - 'a_staticmethod', - 'a_fakemethod', - 'some_assigned_global_ident', - 'some_non_assigned_global_ident', + 'a_method', 'a_method_pep_695', + 'a_classmethod', 'a_classmethod_pep_695', + 'a_staticmethod', 'a_staticmethod_pep_695', + 'a_fakemethod', 'a_fakemethod_pep_695', + 'glob_unassigned_meth', 'glob_unassigned_meth_pep_695', + 'glob_assigned_meth', 'glob_assigned_meth_pep_695', )) def test_filename_correct(self): From fc7f5bbf9599e963d698f4bf5d4efa37e168f3d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 8 Jun 2024 14:33:00 +0200 Subject: [PATCH 10/19] add async def cases --- Lib/test/test_symtable.py | 56 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py index f93385f167cea0..831decaa7176cc 100644 --- a/Lib/test/test_symtable.py +++ b/Lib/test/test_symtable.py @@ -52,16 +52,30 @@ def generic_spam[T](a): class GenericMine[T: int]: pass -# test case: ComplexClass +# 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 @@ -78,29 +92,52 @@ class some_non_method_nested_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 + 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). @@ -108,10 +145,21 @@ def glob_assigned_meth_pep_695[T](): pass 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 """ @@ -305,11 +353,17 @@ def test_class_info(self): 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): From 1352d0856ba7d513d341a29b30675312d98f145c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 8 Jun 2024 20:50:49 +0200 Subject: [PATCH 11/19] update comments --- Lib/symtable.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/symtable.py b/Lib/symtable.py index 07860fd57247de..061700c27f6796 100644 --- a/Lib/symtable.py +++ b/Lib/symtable.py @@ -231,13 +231,15 @@ def is_local_symbol(ident): 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 , - # so we need to find the corresponding inner function, - # class or type alias. + # 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) # 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) From 62d2d6d33ef2ddac81116d2aa2a00c84dac28a31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 9 Jun 2024 12:22:24 +0200 Subject: [PATCH 12/19] prepare for handling PEP 649 --- Lib/symtable.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/Lib/symtable.py b/Lib/symtable.py index 061700c27f6796..150cbff2bef04c 100644 --- a/Lib/symtable.py +++ b/Lib/symtable.py @@ -6,7 +6,7 @@ DEF_GLOBAL, DEF_NONLOCAL, DEF_LOCAL, DEF_PARAM, DEF_TYPE_PARAM, DEF_IMPORT, DEF_BOUND, DEF_ANNOT, SCOPE_OFF, SCOPE_MASK, - FREE, LOCAL, GLOBAL_IMPLICIT, GLOBAL_EXPLICIT, CELL + FREE, LOCAL, GLOBAL_IMPLICIT, GLOBAL_EXPLICIT, CELL, ) import weakref @@ -229,19 +229,17 @@ def is_local_symbol(ident): for st in self._table.children: # 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) - # 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 + 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 From bd055375501a0d18a17b77be65cbb0d8bba7f66f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 9 Jun 2024 12:38:21 +0200 Subject: [PATCH 13/19] unapply restyle --- Lib/symtable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/symtable.py b/Lib/symtable.py index 150cbff2bef04c..e30cb0821d3aca 100644 --- a/Lib/symtable.py +++ b/Lib/symtable.py @@ -6,7 +6,7 @@ DEF_GLOBAL, DEF_NONLOCAL, DEF_LOCAL, DEF_PARAM, DEF_TYPE_PARAM, DEF_IMPORT, DEF_BOUND, DEF_ANNOT, SCOPE_OFF, SCOPE_MASK, - FREE, LOCAL, GLOBAL_IMPLICIT, GLOBAL_EXPLICIT, CELL, + FREE, LOCAL, GLOBAL_IMPLICIT, GLOBAL_EXPLICIT, CELL ) import weakref From 7dbf06c0dcbc31d7c8f43dbfa3f2c445ab958d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 10 Jun 2024 13:39:35 +0200 Subject: [PATCH 14/19] improve test - extract the 'ComplexClass' test case from simple test code - rename some variables in the test code --- Lib/test/test_symtable.py | 71 +++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py index 831decaa7176cc..fbeefa8a095dc7 100644 --- a/Lib/test/test_symtable.py +++ b/Lib/test/test_symtable.py @@ -51,43 +51,48 @@ def generic_spam[T](a): 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: int -glob_unassigned_meth_pep_695: int -glob_unassigned_async_meth: int -glob_unassigned_async_meth_pep_695: int +glob_unassigned_meth: Any +glob_unassigned_meth_pep_695: Any + +glob_unassigned_async_meth: Any +glob_unassigned_async_meth_pep_695: Any -glob_assigned_meth = 1234 -glob_assigned_meth_pep_695 = 1234 -glob_assigned_async_meth = 1234 -glob_assigned_async_meth_pep_695 = 1234 +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 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 +# are not considered as local symbols of ComplexClass). +glob_unassigned_meth_ignore: Any +glob_unassigned_meth_pep_695_ignore: Any -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 +glob_unassigned_async_meth_ignore: Any +glob_unassigned_async_meth_pep_695_ignore: Any -class ComplexClass: - some_non_method_const = 1234 +def glob_assigned_meth_ignore(): pass +def glob_assigned_meth_pep_695_ignore[T](): pass - class some_non_method_nested: pass - class some_non_method_nested_pep_695[T]: pass +async def glob_assigned_async_meth_ignore(): pass +async def glob_assigned_async_meth_pep_695_ignore[T](): pass - type some_non_method_alias = int - type some_non_method_alias_pep_695[T] = list[T] +class ComplexClass: + a_var = 1234 + a_genexpr = (x for x in []) + a_lambda = lambda x: x + + class a_class: pass + class a_class_pep_695[T]: pass - some_non_method_genexpr = (x for x in []) - some_non_method_lambda = lambda x: x + type a_type_alias = int + type a_type_alias_pep_695[T] = list[T] def a_method(self): pass def a_method_pep_695[T](self): pass @@ -111,9 +116,9 @@ def a_staticmethod(): pass def a_staticmethod_pep_695[T](self): pass @staticmethod - def an_async_staticmethod(): pass + async def an_async_staticmethod(): pass @staticmethod - def an_async_staticmethod_pep_695[T](self): pass + 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 @@ -138,9 +143,9 @@ 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). + # 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 @@ -174,7 +179,6 @@ 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") @@ -351,7 +355,10 @@ def test_name(self): def test_class_info(self): self.assertEqual(self.Mine.get_methods(), ('a_method',)) - self.assertEqual(self.ComplexClass.get_methods(), ( + 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', From 55104441679210b53a25b0780588062160001a93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 10 Jun 2024 13:49:31 +0200 Subject: [PATCH 15/19] normalize declaration order --- Lib/test/test_symtable.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py index fbeefa8a095dc7..8c6dcaf3e63698 100644 --- a/Lib/test/test_symtable.py +++ b/Lib/test/test_symtable.py @@ -88,12 +88,12 @@ class ComplexClass: a_genexpr = (x for x in []) a_lambda = lambda x: x - class a_class: pass - class a_class_pep_695[T]: pass - 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 From b13676f0a9fe4bdea9268a4f98e94fccf22c26b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:01:27 +0200 Subject: [PATCH 16/19] improve example --- Doc/library/symtable.rst | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/Doc/library/symtable.rst b/Doc/library/symtable.rst index c1fd1c107ac63b..2c831aa42c02a4 100644 --- a/Doc/library/symtable.rst +++ b/Doc/library/symtable.rst @@ -130,17 +130,36 @@ Examining Symbol Tables 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:: + 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 by :meth:`get_methods`. + + For example: >>> 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] + >>> 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') + >>> _outer, class_A = st.get_children() >>> class_A.get_methods() - ('f', 'g') + >>> ('f', 'g', 'h') + + Although ``A().f()`` raises :exc:`TypeError` at runtime, ``A.f`` is still + considered as a method-like function. .. class:: Symbol From 5227b508d9a446f89ede4abbb9ea9c9e97ff15ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 12 Jun 2024 12:58:58 +0200 Subject: [PATCH 17/19] Merge branch 'main' into fix-symtable-get-methods # Conflicts: # Lib/symtable.py --- Doc/library/symtable.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/symtable.rst b/Doc/library/symtable.rst index 2c831aa42c02a4..03fdbe01ec3bb0 100644 --- a/Doc/library/symtable.rst +++ b/Doc/library/symtable.rst @@ -154,7 +154,7 @@ Examining Symbol Tables ... global outer ... def outer(self): pass ... ''', 'test', 'exec') - >>> _outer, class_A = st.get_children() + >>> class_A = st.get_children()[2] >>> class_A.get_methods() >>> ('f', 'g', 'h') From 26e6038a904a918e6879f6bd67939d8de222e41d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:11:56 +0200 Subject: [PATCH 18/19] Update Doc/library/symtable.rst Co-authored-by: Jelle Zijlstra --- Doc/library/symtable.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/symtable.rst b/Doc/library/symtable.rst index 03fdbe01ec3bb0..4951a1622f71fa 100644 --- a/Doc/library/symtable.rst +++ b/Doc/library/symtable.rst @@ -134,7 +134,7 @@ Examining Symbol Tables body via :keyword:`def` or :keyword:`async def`. Functions defined in a deeper scope (e.g., in an inner class) are not - picked by :meth:`get_methods`. + picked up by :meth:`get_methods`. For example: From f33f99856df593f650f84996da60c904d0c80ffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 12 Jun 2024 14:44:13 +0200 Subject: [PATCH 19/19] fix typo... --- Doc/library/symtable.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/symtable.rst b/Doc/library/symtable.rst index 8ee3dc630bc164..8a0b3ce431e2b9 100644 --- a/Doc/library/symtable.rst +++ b/Doc/library/symtable.rst @@ -156,7 +156,7 @@ Examining Symbol Tables ... ''', 'test', 'exec') >>> class_A = st.get_children()[2] >>> class_A.get_methods() - >>> ('f', 'g', 'h') + ('f', 'g', 'h') Although ``A().f()`` raises :exc:`TypeError` at runtime, ``A.f`` is still considered as a method-like function.