From b696af4d6f9fb1ee40bae7242b199840ffe744b6 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 11 Apr 2024 15:22:02 +0300 Subject: [PATCH 1/5] gh-117764: Add more tests for signatures of builtins Test signatures of all public builtins and methods of builtin classes in modules builtins, types, sys, and several other modules (either included in the list of standard builtin modules sys.builtin_module_names, or providing a public interface for such modules). Most builtins should have supported signatures, with few known exceptions. When more builtins will be converted to Argument Clinic or support of new signatures be implemented, they will be removed from the exception lists. --- Lib/test/test_inspect/test_inspect.py | 290 +++++++++++++++++++++++--- 1 file changed, 256 insertions(+), 34 deletions(-) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 6494842c217662..cc9774bac01041 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -5221,10 +5221,17 @@ class TestSignatureDefinitions(unittest.TestCase): # This test case provides a home for checking that particular APIs # have signatures available for introspection + @staticmethod + def is_public(name): + return not name.startswith('_') or name.startswith('__') and name.endswith('__') + @cpython_only @unittest.skipIf(MISSING_C_DOCSTRINGS, "Signature information for builtins requires docstrings") - def test_builtins_have_signatures(self): + def _test_module_has_signatures(self, module, + no_signature=(), unsupported_signature=(), + methods_no_signature={}, methods_unsupported_signature={}, + good_exceptions=()): # This checks all builtin callables in CPython have signatures # A few have signatures Signature can't yet handle, so we skip those # since they will have to wait until PEP 457 adds the required @@ -5233,48 +5240,263 @@ def test_builtins_have_signatures(self): # reasons, so we also skip those for the time being, but design # the test to fail in order to indicate when it needs to be # updated. - no_signature = set() - # These need PEP 457 groups - needs_groups = {"range", "slice", "dir", "getattr", - "next", "iter", "vars"} - no_signature |= needs_groups - # These have unrepresentable parameter default values of NULL - needs_null = {"anext"} - no_signature |= needs_null - # These need *args support in Argument Clinic - needs_varargs = {"min", "max", "__build_class__"} - no_signature |= needs_varargs - # These builtin types are expected to provide introspection info - types_with_signatures = { - 'bool', 'classmethod', 'complex', 'enumerate', 'filter', 'float', - 'frozenset', 'list', 'map', 'memoryview', 'object', 'property', - 'reversed', 'set', 'staticmethod', 'tuple', 'zip' - } + no_signature = no_signature or set() # Check the signatures we expect to be there - ns = vars(builtins) + ns = vars(module) + try: + names = set(module.__all__) + except AttributeError: + names = set(name for name in ns if self.is_public(name)) for name, obj in sorted(ns.items()): + if name not in names: + continue if not callable(obj): continue - # The builtin types haven't been converted to AC yet - if isinstance(obj, type) and (name not in types_with_signatures): - # Note that this also skips all the exception types + if (isinstance(obj, type) and + issubclass(obj, BaseException) and + name not in good_exceptions): no_signature.add(name) - if (name in no_signature): - # Not yet converted - continue - if name in {'classmethod', 'staticmethod'}: - # Bug gh-112006: inspect.unwrap() does not work with types - # with the __wrapped__ data descriptor. - continue - with self.subTest(builtin=name): - self.assertIsNotNone(inspect.signature(obj)) + if name not in no_signature and name not in unsupported_signature: + with self.subTest('supported', builtin=name): + self.assertIsNotNone(inspect.signature(obj)) + if isinstance(obj, type): + with self.subTest(type=name): + self._test_builtin_methods_have_signatures(obj, + methods_no_signature.get(name, ()), + methods_unsupported_signature.get(name, ())) # Check callables that haven't been converted don't claim a signature # This ensures this test will start failing as more signatures are # added, so the affected items can be moved into the scope of the # regression test above - for name in no_signature - needs_null: - with self.subTest(builtin=name): - self.assertIsNone(ns[name].__text_signature__) + for name in no_signature: + with self.subTest('none', builtin=name): + obj = ns[name] + self.assertIsNone(obj.__text_signature__) + self.assertRaises(ValueError, inspect.signature, obj) + for name in unsupported_signature: + with self.subTest('unsupported', builtin=name): + obj = ns[name] + self.assertIsNotNone(obj.__text_signature__) + self.assertRaises(ValueError, inspect.signature, obj) + + def _test_builtin_methods_have_signatures(self, cls, no_signature, unsupported_signature): + ns = vars(cls) + for name in ns: + obj = getattr(cls, name, None) + if not callable(obj) or isinstance(obj, type): + continue + if name not in no_signature and name not in unsupported_signature: + with self.subTest('supported', method=name): + self.assertIsNotNone(inspect.signature(obj)) + for name in no_signature: + with self.subTest('none', method=name): + self.assertIsNone(getattr(cls, name).__text_signature__) + self.assertRaises(ValueError, inspect.signature, getattr(cls, name)) + for name in unsupported_signature: + with self.subTest('unsupported', method=name): + self.assertIsNotNone(getattr(cls, name).__text_signature__) + self.assertRaises(ValueError, inspect.signature, getattr(cls, name)) + + def test_builtins_have_signatures(self): + no_signature = {'type', 'super', 'bytearray', 'bytes', 'dict', 'int', 'str'} + # These need PEP 457 groups + needs_groups = {"range", "slice", "dir", "getattr", + "next", "iter", "vars"} + no_signature |= needs_groups + # These have unrepresentable parameter default values of NULL + unsupported_signature = {"anext"} + # These need *args support in Argument Clinic + needs_varargs = {"min", "max", "__build_class__"} + no_signature |= needs_varargs + + methods_no_signature = { + 'dict': {'update'}, + 'object': {'__class__'}, + } + methods_unsupported_signature = { + 'bytearray': {'count', 'endswith', 'find', 'hex', 'index', 'rfind', 'rindex', 'startswith'}, + 'bytes': {'count', 'endswith', 'find', 'hex', 'index', 'rfind', 'rindex', 'startswith'}, + 'dict': {'pop'}, + 'int': {'__round__'}, + 'memoryview': {'cast', 'hex'}, + 'str': {'count', 'endswith', 'find', 'index', 'maketrans', 'rfind', 'rindex', 'startswith'}, + } + self._test_module_has_signatures(builtins, + no_signature, unsupported_signature, + methods_no_signature, methods_unsupported_signature) + + def test_types_module_has_signatures(self): + unsupported_signature = {'CellType'} + methods_no_signature = { + 'AsyncGeneratorType': {'athrow'}, + 'CoroutineType': {'throw'}, + 'GeneratorType': {'throw'}, + } + self._test_module_has_signatures(types, + unsupported_signature=unsupported_signature, + methods_no_signature=methods_no_signature) + + def test_sys_module_has_signatures(self): + no_signature = {'getsizeof', 'set_asyncgen_hooks'} + self._test_module_has_signatures(sys, no_signature) + + def test_abc_module_has_signatures(self): + import abc + self._test_module_has_signatures(abc) + + def test_atexit_module_has_signatures(self): + import atexit + self._test_module_has_signatures(atexit) + + def test_codecs_module_has_signatures(self): + import codecs + methods_no_signature = {'StreamReader': {'charbuffertype'}} + self._test_module_has_signatures(codecs, + methods_no_signature=methods_no_signature) + + def test_collections_module_has_signatures(self): + no_signature = {'OrderedDict', 'defaultdict'} + unsupported_signature = {'deque'} + methods_no_signature = { + 'OrderedDict': {'update'}, + } + methods_unsupported_signature = { + 'deque': {'index'}, + 'OrderedDict': {'pop'}, + 'UserString': {'maketrans'}, + } + self._test_module_has_signatures(collections, + no_signature, unsupported_signature, + methods_no_signature, methods_unsupported_signature) + + def test_collections_abc_module_has_signatures(self): + import collections.abc + self._test_module_has_signatures(collections.abc) + + def test_errno_module_has_signatures(self): + import errno + self._test_module_has_signatures(errno) + + def test_faulthandler_module_has_signatures(self): + import faulthandler + unsupported_signature = {'dump_traceback', 'dump_traceback_later', 'enable', 'register'} + self._test_module_has_signatures(faulthandler, unsupported_signature=unsupported_signature) + + def test_functools_module_has_signatures(self): + no_signature = {'reduce'} + self._test_module_has_signatures(functools, no_signature) + + def test_gc_module_has_signatures(self): + import gc + no_signature = {'set_threshold'} + self._test_module_has_signatures(gc, no_signature) + + def test_io_module_has_signatures(self): + methods_no_signature = { + 'BufferedRWPair': {'read', 'peek', 'read1', 'readinto', 'readinto1', 'write'}, + } + self._test_module_has_signatures(io, + methods_no_signature=methods_no_signature) + + def test_itertools_module_has_signatures(self): + import itertools + no_signature = {'islice', 'repeat'} + self._test_module_has_signatures(itertools, no_signature) + + def test_locale_module_has_signatures(self): + import locale + self._test_module_has_signatures(locale) + + def test_marshal_module_has_signatures(self): + import marshal + self._test_module_has_signatures(marshal) + + def test_operator_module_has_signatures(self): + import operator + self._test_module_has_signatures(operator) + + def test_os_module_has_signatures(self): + unsupported_signature = {'chmod', 'get_terminal_size', 'posix_spawn', 'posix_spawnp', 'register_at_fork', 'utime'} + self._test_module_has_signatures(os, unsupported_signature=unsupported_signature) + + def test_pwd_module_has_signatures(self): + import pwd + self._test_module_has_signatures(pwd) + + def test_re_module_has_signatures(self): + import re + methods_no_signature = {'Match': {'group'}} + self._test_module_has_signatures(re, + methods_no_signature=methods_no_signature, + good_exceptions={'error', 'PatternError'}) + + def test_signal_module_has_signatures(self): + import signal + self._test_module_has_signatures(signal) + + def test_stat_module_has_signatures(self): + import stat + self._test_module_has_signatures(stat) + + def test_string_module_has_signatures(self): + import string + self._test_module_has_signatures(string) + + def test_symtable_module_has_signatures(self): + import symtable + self._test_module_has_signatures(symtable) + + def test_sysconfig_module_has_signatures(self): + import sysconfig + self._test_module_has_signatures(sysconfig) + + def test_threading_module_has_signatures(self): + import threading + self._test_module_has_signatures(threading) + + def test_thread_module_has_signatures(self): + import _thread + no_signature = {'RLock'} + self._test_module_has_signatures(_thread, no_signature) + + def test_time_module_has_signatures(self): + no_signature = { + 'asctime', 'clock_getres', 'clock_settime', 'clock_settime_ns', + 'ctime', 'get_clock_info', 'gmtime', 'localtime', + 'pthread_getcpuclockid', 'strftime', 'strptime' + } + self._test_module_has_signatures(time, no_signature) + + def test_tokenize_module_has_signatures(self): + import tokenize + self._test_module_has_signatures(tokenize) + + def test_tracemalloc_module_has_signatures(self): + import tracemalloc + self._test_module_has_signatures(tracemalloc) + + def test_typing_module_has_signatures(self): + import typing + no_signature = {'ParamSpec', 'ParamSpecArgs', 'ParamSpecKwargs', + 'Text', 'TypeAliasType', 'TypeVar', 'TypeVarTuple'} + methods_no_signature = { + 'Generic': {'__class_getitem__', '__init_subclass__'}, + } + methods_unsupported_signature = { + 'Text': {'count', 'find', 'index', 'rfind', 'rindex', 'startswith', 'endswith', 'maketrans'}, + } + self._test_module_has_signatures(typing, no_signature, + methods_no_signature=methods_no_signature, + methods_unsupported_signature=methods_unsupported_signature) + + def test_warnings_module_has_signatures(self): + unsupported_signature = {'warn', 'warn_explicit'} + self._test_module_has_signatures(warnings, unsupported_signature=unsupported_signature) + + def test_weakref_module_has_signatures(self): + import weakref + no_signature = {'ReferenceType', 'ref'} + self._test_module_has_signatures(weakref, no_signature) def test_python_function_override_signature(self): def func(*args, **kwargs): From a73d99cf9f514143db681ae8090c0e080912e698 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 12 Apr 2024 16:34:28 +0300 Subject: [PATCH 2/5] Fix tests on Windows and macOS. --- Lib/test/test_inspect/test_inspect.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index cc9774bac01041..207e0dddca6af9 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -5379,7 +5379,9 @@ def test_errno_module_has_signatures(self): def test_faulthandler_module_has_signatures(self): import faulthandler - unsupported_signature = {'dump_traceback', 'dump_traceback_later', 'enable', 'register'} + unsupported_signature = {'dump_traceback', 'dump_traceback_later', 'enable'} + if os.name == 'posix': + unsupported_signature |= {'register'} self._test_module_has_signatures(faulthandler, unsupported_signature=unsupported_signature) def test_functools_module_has_signatures(self): @@ -5416,11 +5418,15 @@ def test_operator_module_has_signatures(self): self._test_module_has_signatures(operator) def test_os_module_has_signatures(self): - unsupported_signature = {'chmod', 'get_terminal_size', 'posix_spawn', 'posix_spawnp', 'register_at_fork', 'utime'} + unsupported_signature = {'chmod', 'get_terminal_size', 'utime'} + if os.name == 'posix': + unsupported_signature |= {'posix_spawn', 'posix_spawnp', 'register_at_fork'} + if os.name == 'nt': + unsupported_signature |= {'startfile'} self._test_module_has_signatures(os, unsupported_signature=unsupported_signature) def test_pwd_module_has_signatures(self): - import pwd + pwd = import_helper.import_module('pwd') self._test_module_has_signatures(pwd) def test_re_module_has_signatures(self): @@ -5461,10 +5467,14 @@ def test_thread_module_has_signatures(self): def test_time_module_has_signatures(self): no_signature = { - 'asctime', 'clock_getres', 'clock_settime', 'clock_settime_ns', - 'ctime', 'get_clock_info', 'gmtime', 'localtime', - 'pthread_getcpuclockid', 'strftime', 'strptime' + 'asctime', 'ctime', 'get_clock_info', 'gmtime', 'localtime', + 'strftime', 'strptime' } + if os.name == 'posix': + no_signature |= {'clock_getres', 'clock_settime', 'clock_settime_ns'} + for name in ['pthread_getcpuclockid']: + if hasattr(os, name): + no_signature.add(name) self._test_module_has_signatures(time, no_signature) def test_tokenize_module_has_signatures(self): From f8e13c0b3c9d23f7c1ba639d2c56549242c895f9 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 12 Apr 2024 16:39:57 +0300 Subject: [PATCH 3/5] Fix the previous fix on Linux. --- Lib/test/test_inspect/test_inspect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 207e0dddca6af9..576de0d906f5af 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -5473,7 +5473,7 @@ def test_time_module_has_signatures(self): if os.name == 'posix': no_signature |= {'clock_getres', 'clock_settime', 'clock_settime_ns'} for name in ['pthread_getcpuclockid']: - if hasattr(os, name): + if hasattr(time, name): no_signature.add(name) self._test_module_has_signatures(time, no_signature) From 5fe84940090f056ca260c34a5688518185ba4a2d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 12 Apr 2024 17:17:03 +0300 Subject: [PATCH 4/5] Fix tests on wasm. --- Lib/test/test_inspect/test_inspect.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 576de0d906f5af..089fa575ecfc90 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -5380,8 +5380,8 @@ def test_errno_module_has_signatures(self): def test_faulthandler_module_has_signatures(self): import faulthandler unsupported_signature = {'dump_traceback', 'dump_traceback_later', 'enable'} - if os.name == 'posix': - unsupported_signature |= {'register'} + unsupported_signature |= {name for name in ['register'] + if hasattr(faulthandler, name)} self._test_module_has_signatures(faulthandler, unsupported_signature=unsupported_signature) def test_functools_module_has_signatures(self): @@ -5419,10 +5419,10 @@ def test_operator_module_has_signatures(self): def test_os_module_has_signatures(self): unsupported_signature = {'chmod', 'get_terminal_size', 'utime'} - if os.name == 'posix': - unsupported_signature |= {'posix_spawn', 'posix_spawnp', 'register_at_fork'} - if os.name == 'nt': - unsupported_signature |= {'startfile'} + unsupported_signature |= {name for name in + ['get_terminal_size', 'posix_spawn', 'posix_spawnp', + 'register_at_fork', 'startfile'] + if hasattr(os, name)} self._test_module_has_signatures(os, unsupported_signature=unsupported_signature) def test_pwd_module_has_signatures(self): @@ -5470,11 +5470,10 @@ def test_time_module_has_signatures(self): 'asctime', 'ctime', 'get_clock_info', 'gmtime', 'localtime', 'strftime', 'strptime' } - if os.name == 'posix': - no_signature |= {'clock_getres', 'clock_settime', 'clock_settime_ns'} - for name in ['pthread_getcpuclockid']: - if hasattr(time, name): - no_signature.add(name) + no_signature |= {name for name in + ['clock_getres', 'clock_settime', 'clock_settime_ns', + 'pthread_getcpuclockid'] + if hasattr(time, name)} self._test_module_has_signatures(time, no_signature) def test_tokenize_module_has_signatures(self): From 14a6ea2b9462277386faa472ccea6b28d5537109 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 12 Apr 2024 17:25:48 +0300 Subject: [PATCH 5/5] Final fix. --- Lib/test/test_inspect/test_inspect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 089fa575ecfc90..e8b09c413f12da 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -5418,7 +5418,7 @@ def test_operator_module_has_signatures(self): self._test_module_has_signatures(operator) def test_os_module_has_signatures(self): - unsupported_signature = {'chmod', 'get_terminal_size', 'utime'} + unsupported_signature = {'chmod', 'utime'} unsupported_signature |= {name for name in ['get_terminal_size', 'posix_spawn', 'posix_spawnp', 'register_at_fork', 'startfile']