From aa8865e46dc7da667ddecd2c138a370095930f06 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Mon, 3 Jun 2024 08:17:20 +0800 Subject: [PATCH 01/24] Preserve class signature after wrapping with `@warnings.deprecated` --- Lib/warnings.py | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/Lib/warnings.py b/Lib/warnings.py index df844253ab4e6d..edb49e8595dacd 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -590,17 +590,49 @@ def __call__(self, arg, /): if category is None: arg.__deprecated__ = msg return arg - elif isinstance(arg, type): + + def update_signature(original_func): + # Ensure that the signature of the decorated callable matches the original one + + def wrapper(func): + import inspect + + try: + original_signature = inspect.signature(original_func) + except ValueError: + pass + else: + signature = inspect.signature(func) + if signature != original_signature: + try: + func.__text_signature__ = str(original_signature) + except (AttributeError, TypeError): + pass + + return func + + return wrapper + + if isinstance(arg, type): import functools from types import MethodType original_new = arg.__new__ + is_object_new = original_new is object.__new__ + if is_object_new: + def wraps(wrapped): + def identity(func): + return func + return identity + else: + wraps = functools.wraps - @functools.wraps(original_new) + @update_signature(arg) + @wraps(original_new) def __new__(cls, *args, **kwargs): if cls is arg: warn(msg, category=category, stacklevel=stacklevel + 1) - if original_new is not object.__new__: + if not is_object_new: return original_new(cls, *args, **kwargs) # Mirrors a similar check in object.__new__. elif cls.__init__ is object.__init__ and (args or kwargs): @@ -616,15 +648,19 @@ def __new__(cls, *args, **kwargs): if isinstance(original_init_subclass, MethodType): original_init_subclass = original_init_subclass.__func__ + @update_signature(original_init_subclass) @functools.wraps(original_init_subclass) def __init_subclass__(*args, **kwargs): warn(msg, category=category, stacklevel=stacklevel + 1) return original_init_subclass(*args, **kwargs) arg.__init_subclass__ = classmethod(__init_subclass__) + # Or otherwise, which likely means it's a builtin such as # object's implementation of __init_subclass__. else: + + @update_signature(original_init_subclass) @functools.wraps(original_init_subclass) def __init_subclass__(*args, **kwargs): warn(msg, category=category, stacklevel=stacklevel + 1) @@ -639,6 +675,7 @@ def __init_subclass__(*args, **kwargs): import functools import inspect + @update_signature(arg) @functools.wraps(arg) def wrapper(*args, **kwargs): warn(msg, category=category, stacklevel=stacklevel + 1) From d093b57071973b2339558d8e5cde2e8a7a962973 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 3 Apr 2025 17:19:45 +0000 Subject: [PATCH 02/24] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst diff --git a/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst b/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst new file mode 100644 index 00000000000000..2f73e16040e977 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst @@ -0,0 +1 @@ +Preserve class signature after wrapping with :func:`warnings.deprecated`. Patch by Xuehai Pan. From 96642117c436ceca624920e7b987d27998c4db23 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Fri, 4 Apr 2025 01:45:42 +0800 Subject: [PATCH 03/24] Add test to class signature --- Lib/test/test_warnings/__init__.py | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index 6f4c569d247601..e76bcfcdbee06c 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -1832,6 +1832,50 @@ async def coro(self): self.assertFalse(inspect.iscoroutinefunction(Cls.sync)) self.assertTrue(inspect.iscoroutinefunction(Cls.coro)) + def test_inspect_class_signature(self): + class Cls1: # no __init__ or __new__ + pass + + class Cls2: # __new__ only + def __new__(cls, x, y): + return super().__new__(cls) + + class Cls3: # __init__ only + def __init__(self, x, y): + pass + + class Cls4: # __new__ and __init__ + def __new__(cls, x, y): + return super().__new__(cls) + + def __init__(self, x, y): + pass + + class Cls5(Cls1): # inherits no __init__ or __new__ + pass + + class Cls6(Cls2): # inherits __new__ only + pass + + class Cls7(Cls3): # inherits __init__ only + pass + + class Cls8(Cls4): # inherits __new__ and __init__ + pass + + for cls in (Cls1, Cls2, Cls3, Cls4, Cls5, Cls6, Cls7, Cls8): + with self.subTest(f'{cls.__name__}'): + try: + original_signature = inspect.signature(cls) + except ValueError: + original_signature = None + try: + new_signature = inspect.signature(deprecated("depr")(cls)) + except ValueError: + new_signature = None + self.assertEqual(original_signature, new_signature) + + def setUpModule(): py_warnings.onceregistry.clear() c_warnings.onceregistry.clear() From a1dd52e067f546a6ddbff86db1ac65741fe3cd7d Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Fri, 4 Apr 2025 02:43:24 +0800 Subject: [PATCH 04/24] Fix tests --- Lib/test/test_warnings/__init__.py | 12 +++++++++++- Lib/warnings.py | 10 +++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index e76bcfcdbee06c..d2ad2a579f4e1f 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -1864,7 +1864,7 @@ class Cls8(Cls4): # inherits __new__ and __init__ pass for cls in (Cls1, Cls2, Cls3, Cls4, Cls5, Cls6, Cls7, Cls8): - with self.subTest(f'{cls.__name__}'): + with self.subTest(f'class {cls.__name__} signature'): try: original_signature = inspect.signature(cls) except ValueError: @@ -1875,6 +1875,16 @@ class Cls8(Cls4): # inherits __new__ and __init__ new_signature = None self.assertEqual(original_signature, new_signature) + with self.subTest(f'class {cls.__name__}.__new__ signature'): + try: + original_signature = inspect.signature(cls.__new__) + except ValueError: + original_signature = None + try: + new_signature = inspect.signature(deprecated("depr")(cls).__new__) + except ValueError: + new_signature = None + self.assertEqual(original_signature, new_signature) def setUpModule(): py_warnings.onceregistry.clear() diff --git a/Lib/warnings.py b/Lib/warnings.py index edb49e8595dacd..305a1988e9a231 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -620,14 +620,22 @@ def wrapper(func): original_new = arg.__new__ is_object_new = original_new is object.__new__ if is_object_new: + import inspect + + try: + arg.__signature__ = inspect.signature(arg) + except ValueError: + pass + def wraps(wrapped): def identity(func): return func return identity + else: wraps = functools.wraps - @update_signature(arg) + @update_signature(original_new) @wraps(original_new) def __new__(cls, *args, **kwargs): if cls is arg: From 68eebadb3ce125e582b0127dde7f1a79fed8397c Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Fri, 4 Apr 2025 05:08:06 +0800 Subject: [PATCH 05/24] Handle potential assignment failure --- Lib/warnings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/warnings.py b/Lib/warnings.py index 305a1988e9a231..2d97cb8a1a9c76 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -624,7 +624,7 @@ def wrapper(func): try: arg.__signature__ = inspect.signature(arg) - except ValueError: + except (ValueError, AttributeError, TypeError): pass def wraps(wrapped): From 57c61c37d543d6d47a69168bb79757242d79e523 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Fri, 4 Apr 2025 15:06:39 +0800 Subject: [PATCH 06/24] Update tests --- Lib/test/test_warnings/__init__.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index d2ad2a579f4e1f..a9e07a07b38bb2 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -1870,26 +1870,31 @@ class Cls8(Cls4): # inherits __new__ and __init__ except ValueError: original_signature = None try: - new_signature = inspect.signature(deprecated("depr")(cls)) + original_new_signature = inspect.signature(cls.__new__) except ValueError: - new_signature = None - self.assertEqual(original_signature, new_signature) + original_new_signature = None + + deprecated_cls = deprecated("depr")(cls) - with self.subTest(f'class {cls.__name__}.__new__ signature'): try: - original_signature = inspect.signature(cls.__new__) + deprecated_signature = inspect.signature(deprecated_cls) except ValueError: - original_signature = None + deprecated_signature = None + self.assertEqual(original_signature, deprecated_signature) try: - new_signature = inspect.signature(deprecated("depr")(cls).__new__) + deprecated_new_signature = inspect.signature(deprecated_cls.__new__) except ValueError: - new_signature = None - self.assertEqual(original_signature, new_signature) + deprecated_new_signature = None + + self.assertEqual(original_signature, deprecated_signature) + self.assertEqual(original_new_signature, deprecated_new_signature) + def setUpModule(): py_warnings.onceregistry.clear() c_warnings.onceregistry.clear() + tearDownModule = setUpModule if __name__ == "__main__": From 6e28f0b03076f9e9f230ae7d74ae026461903f11 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Fri, 4 Apr 2025 19:17:07 +0800 Subject: [PATCH 07/24] Use `__signature__` --- Lib/warnings.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Lib/warnings.py b/Lib/warnings.py index 2d97cb8a1a9c76..45a63768bb7e16 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -604,10 +604,7 @@ def wrapper(func): else: signature = inspect.signature(func) if signature != original_signature: - try: - func.__text_signature__ = str(original_signature) - except (AttributeError, TypeError): - pass + func.__signature__ = original_signature return func From 43b0977de08595600e23cbccc0fd0d07eb480cee Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Fri, 4 Apr 2025 19:23:45 +0800 Subject: [PATCH 08/24] Remove duplicate in tests --- Lib/test/test_warnings/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index a9e07a07b38bb2..1762a4f80f3d74 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -1881,12 +1881,11 @@ class Cls8(Cls4): # inherits __new__ and __init__ except ValueError: deprecated_signature = None self.assertEqual(original_signature, deprecated_signature) + try: deprecated_new_signature = inspect.signature(deprecated_cls.__new__) except ValueError: deprecated_new_signature = None - - self.assertEqual(original_signature, deprecated_signature) self.assertEqual(original_new_signature, deprecated_new_signature) From 4dd8b709bd5bf8ad36eb721f94e331984a5ed57e Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Fri, 4 Apr 2025 23:42:57 +0800 Subject: [PATCH 09/24] Unwrap decorated __new__ and __init__ in inspect --- Lib/inspect.py | 2 +- Lib/test/test_warnings/__init__.py | 4 ++- Lib/warnings.py | 41 +----------------------------- 3 files changed, 5 insertions(+), 42 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index fcfe3b191ab503..41d2c0737f7fbb 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1916,7 +1916,7 @@ def _signature_get_user_defined_method(cls, method_name): return None if method_name != '__new__': meth = _descriptor_get(meth, cls) - return meth + return unwrap(meth) def _signature_get_partial(wrapped_sig, partial, extra_args=()): diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index 1762a4f80f3d74..ff9b1ba4ef900f 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -1863,7 +1863,9 @@ class Cls7(Cls3): # inherits __init__ only class Cls8(Cls4): # inherits __new__ and __init__ pass - for cls in (Cls1, Cls2, Cls3, Cls4, Cls5, Cls6, Cls7, Cls8): + # The `@deprecated` decorator will update the class in-place. + # Test the children classes first. + for cls in (Cls8, Cls7, Cls6, Cls5, Cls4, Cls3, Cls2, Cls1): with self.subTest(f'class {cls.__name__} signature'): try: original_signature = inspect.signature(cls) diff --git a/Lib/warnings.py b/Lib/warnings.py index 45a63768bb7e16..79bb7340ea8cfe 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -591,49 +591,14 @@ def __call__(self, arg, /): arg.__deprecated__ = msg return arg - def update_signature(original_func): - # Ensure that the signature of the decorated callable matches the original one - - def wrapper(func): - import inspect - - try: - original_signature = inspect.signature(original_func) - except ValueError: - pass - else: - signature = inspect.signature(func) - if signature != original_signature: - func.__signature__ = original_signature - - return func - - return wrapper - if isinstance(arg, type): import functools from types import MethodType original_new = arg.__new__ is_object_new = original_new is object.__new__ - if is_object_new: - import inspect - - try: - arg.__signature__ = inspect.signature(arg) - except (ValueError, AttributeError, TypeError): - pass - - def wraps(wrapped): - def identity(func): - return func - return identity - else: - wraps = functools.wraps - - @update_signature(original_new) - @wraps(original_new) + @functools.wraps(original_new) def __new__(cls, *args, **kwargs): if cls is arg: warn(msg, category=category, stacklevel=stacklevel + 1) @@ -653,7 +618,6 @@ def __new__(cls, *args, **kwargs): if isinstance(original_init_subclass, MethodType): original_init_subclass = original_init_subclass.__func__ - @update_signature(original_init_subclass) @functools.wraps(original_init_subclass) def __init_subclass__(*args, **kwargs): warn(msg, category=category, stacklevel=stacklevel + 1) @@ -664,8 +628,6 @@ def __init_subclass__(*args, **kwargs): # Or otherwise, which likely means it's a builtin such as # object's implementation of __init_subclass__. else: - - @update_signature(original_init_subclass) @functools.wraps(original_init_subclass) def __init_subclass__(*args, **kwargs): warn(msg, category=category, stacklevel=stacklevel + 1) @@ -680,7 +642,6 @@ def __init_subclass__(*args, **kwargs): import functools import inspect - @update_signature(arg) @functools.wraps(arg) def wrapper(*args, **kwargs): warn(msg, category=category, stacklevel=stacklevel + 1) From 6b7bd716e03675cce45e8410b104131d84347cda Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Fri, 4 Apr 2025 23:49:38 +0800 Subject: [PATCH 10/24] Unwrap decorated __new__ and __init__ in inspect --- Lib/inspect.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/inspect.py b/Lib/inspect.py index 41d2c0737f7fbb..f23065101ec8b3 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1910,6 +1910,8 @@ def _signature_get_user_defined_method(cls, method_name): meth = getattr(cls, method_name, None) else: meth = getattr_static(cls, method_name, None) + if meth is not None: + meth = unwrap(meth) if meth is None or isinstance(meth, _NonUserDefinedCallables): # Once '__signature__' will be added to 'C'-level # callables, this check won't be necessary From ca8aa96eba1f7a4bd9b460c17cc8de417a4721e8 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sat, 5 Apr 2025 00:51:26 +0800 Subject: [PATCH 11/24] Fix tests --- Lib/inspect.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index f23065101ec8b3..99ae42554fa60b 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2513,21 +2513,23 @@ def _signature_from_callable(obj, *, if call is not None: return _get_signature_of(call) - new = _signature_get_user_defined_method(obj, '__new__') - init = _signature_get_user_defined_method(obj, '__init__') + # NOTE: The user-defined method can be a function with a thin wrapper + # around object.__new__ (e.g., generated by `@warnings.deprecated`) + unwrapped_new = _signature_get_user_defined_method(obj, '__new__') + unwrapped_init = _signature_get_user_defined_method(obj, '__init__') # Go through the MRO and see if any class has user-defined # pure Python __new__ or __init__ method for base in obj.__mro__: # Now we check if the 'obj' class has an own '__new__' method - if new is not None and '__new__' in base.__dict__: - sig = _get_signature_of(new) + if unwrapped_new is not None and '__new__' in base.__dict__: + sig = _get_signature_of(unwrapped_new) if skip_bound_arg: sig = _signature_bound_method(sig) return sig # or an own '__init__' method - elif init is not None and '__init__' in base.__dict__: - return _get_signature_of(init) + elif unwrapped_init is not None and '__init__' in base.__dict__: + return _get_signature_of(unwrapped_init) # At this point we know, that `obj` is a class, with no user- # defined '__init__', '__new__', or class-level '__call__' @@ -2556,8 +2558,8 @@ def _signature_from_callable(obj, *, if type not in obj.__mro__: # We have a class (not metaclass), but no user-defined # __init__ or __new__ for it - if (obj.__init__ is object.__init__ and - obj.__new__ is object.__new__): + if (unwrap(obj.__init__) is object.__init__ and + unwrap(obj.__new__) is object.__new__): # Return a signature of 'object' builtin. return sigcls.from_callable(object) else: From 636c6875d31b0cbffc3d6b0997e857ac1dab464c Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sat, 5 Apr 2025 01:26:28 +0800 Subject: [PATCH 12/24] Fix tests --- Lib/inspect.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 99ae42554fa60b..2b423028329f1d 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1910,15 +1910,13 @@ def _signature_get_user_defined_method(cls, method_name): meth = getattr(cls, method_name, None) else: meth = getattr_static(cls, method_name, None) - if meth is not None: - meth = unwrap(meth) - if meth is None or isinstance(meth, _NonUserDefinedCallables): + if meth is None or isinstance(unwrap(meth), _NonUserDefinedCallables): # Once '__signature__' will be added to 'C'-level # callables, this check won't be necessary return None if method_name != '__new__': meth = _descriptor_get(meth, cls) - return unwrap(meth) + return meth def _signature_get_partial(wrapped_sig, partial, extra_args=()): @@ -2515,21 +2513,21 @@ def _signature_from_callable(obj, *, # NOTE: The user-defined method can be a function with a thin wrapper # around object.__new__ (e.g., generated by `@warnings.deprecated`) - unwrapped_new = _signature_get_user_defined_method(obj, '__new__') - unwrapped_init = _signature_get_user_defined_method(obj, '__init__') + new = _signature_get_user_defined_method(obj, '__new__') + init = _signature_get_user_defined_method(obj, '__init__') # Go through the MRO and see if any class has user-defined # pure Python __new__ or __init__ method for base in obj.__mro__: # Now we check if the 'obj' class has an own '__new__' method - if unwrapped_new is not None and '__new__' in base.__dict__: - sig = _get_signature_of(unwrapped_new) + if new is not None and '__new__' in base.__dict__: + sig = _get_signature_of(new) if skip_bound_arg: sig = _signature_bound_method(sig) return sig # or an own '__init__' method - elif unwrapped_init is not None and '__init__' in base.__dict__: - return _get_signature_of(unwrapped_init) + elif init is not None and '__init__' in base.__dict__: + return _get_signature_of(init) # At this point we know, that `obj` is a class, with no user- # defined '__init__', '__new__', or class-level '__call__' From 198f8eea054bccbc1999caf055f2d07b2c11242a Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sun, 6 Apr 2025 23:04:41 +0800 Subject: [PATCH 13/24] Revert unneeded changes --- Lib/warnings.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Lib/warnings.py b/Lib/warnings.py index 79bb7340ea8cfe..df844253ab4e6d 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -590,19 +590,17 @@ def __call__(self, arg, /): if category is None: arg.__deprecated__ = msg return arg - - if isinstance(arg, type): + elif isinstance(arg, type): import functools from types import MethodType original_new = arg.__new__ - is_object_new = original_new is object.__new__ @functools.wraps(original_new) def __new__(cls, *args, **kwargs): if cls is arg: warn(msg, category=category, stacklevel=stacklevel + 1) - if not is_object_new: + if original_new is not object.__new__: return original_new(cls, *args, **kwargs) # Mirrors a similar check in object.__new__. elif cls.__init__ is object.__init__ and (args or kwargs): @@ -624,7 +622,6 @@ def __init_subclass__(*args, **kwargs): return original_init_subclass(*args, **kwargs) arg.__init_subclass__ = classmethod(__init_subclass__) - # Or otherwise, which likely means it's a builtin such as # object's implementation of __init_subclass__. else: From 2c50424f0d05ac131dbbdd33c3a7e01ce8423f4e Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sun, 6 Apr 2025 23:10:22 +0800 Subject: [PATCH 14/24] Respect `follow_wrapper_chains` --- Lib/inspect.py | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 2b423028329f1d..95590c9dee457d 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1901,7 +1901,7 @@ def getasyncgenlocals(agen): types.BuiltinFunctionType) -def _signature_get_user_defined_method(cls, method_name): +def _signature_get_user_defined_method(cls, method_name, *, follow_wrapper_chains=True): """Private helper. Checks if ``cls`` has an attribute named ``method_name`` and returns it only if it is a pure python function. @@ -1910,7 +1910,9 @@ def _signature_get_user_defined_method(cls, method_name): meth = getattr(cls, method_name, None) else: meth = getattr_static(cls, method_name, None) - if meth is None or isinstance(unwrap(meth), _NonUserDefinedCallables): + if meth is not None and follow_wrapper_chains: + meth = unwrap(meth) + if meth is None or isinstance(meth, _NonUserDefinedCallables): # Once '__signature__' will be added to 'C'-level # callables, this check won't be necessary return None @@ -2507,14 +2509,26 @@ def _signature_from_callable(obj, *, # First, let's see if it has an overloaded __call__ defined # in its metaclass - call = _signature_get_user_defined_method(type(obj), '__call__') + call = _signature_get_user_defined_method( + type(obj), + '__call__', + follow_wrapper_chains=follow_wrapper_chains, + ) if call is not None: return _get_signature_of(call) # NOTE: The user-defined method can be a function with a thin wrapper # around object.__new__ (e.g., generated by `@warnings.deprecated`) - new = _signature_get_user_defined_method(obj, '__new__') - init = _signature_get_user_defined_method(obj, '__init__') + new = _signature_get_user_defined_method( + obj, + '__new__', + follow_wrapper_chains=follow_wrapper_chains, + ) + init = _signature_get_user_defined_method( + obj, + '__init__', + follow_wrapper_chains=follow_wrapper_chains, + ) # Go through the MRO and see if any class has user-defined # pure Python __new__ or __init__ method @@ -2554,10 +2568,15 @@ def _signature_from_callable(obj, *, # Last option is to check if its '__init__' is # object.__init__ or type.__init__. if type not in obj.__mro__: + obj_init = obj.__init__ + obj_new = obj.__new__ + if follow_wrapper_chains: + obj_init = unwrap(obj_init) + obj_new = unwrap(obj_new) # We have a class (not metaclass), but no user-defined # __init__ or __new__ for it - if (unwrap(obj.__init__) is object.__init__ and - unwrap(obj.__new__) is object.__new__): + if (obj_init is object.__init__ and + obj_new is object.__new__): # Return a signature of 'object' builtin. return sigcls.from_callable(object) else: From c62184ad82711a42cd59194d62e708d166816908 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sun, 6 Apr 2025 23:12:37 +0800 Subject: [PATCH 15/24] Update news entry --- .../Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst b/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst index 2f73e16040e977..dd62d61212d44c 100644 --- a/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst +++ b/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst @@ -1 +1,4 @@ -Preserve class signature after wrapping with :func:`warnings.deprecated`. Patch by Xuehai Pan. +Unwrap :meth:`__new__` and :meth:`__init__` methods when getting the class +signature for a class with :func:`inspect.signature`. +Preserve class signature after wrapping with :func:`warnings.deprecated`. +Patch by Xuehai Pan. From 759d403725ed6120749fd0ac2532fc7d52b82a7f Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sun, 6 Apr 2025 23:34:01 +0800 Subject: [PATCH 16/24] Add test for inspect --- Lib/test/test_inspect/test_inspect.py | 40 ++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index daae990458d708..bfe432eb31baee 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -3845,7 +3845,6 @@ def wrapped_foo_call(): ('b', ..., ..., "positional_or_keyword")), ...)) - def test_signature_on_class(self): class C: def __init__(self, a): @@ -4019,6 +4018,45 @@ def __init__(self, b): ('bar', 2, ..., "keyword_only")), ...)) + def test_signature_on_class_with_decorated_new(self): + def identity(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + + class Foo: + @identity + def __new__(cls, a, b): + pass + + self.assertEqual(self.signature(Foo), + ((('a', ..., ..., "positional_or_keyword"), + ('b', ..., ..., "positional_or_keyword")), + ...)) + + self.assertEqual(self.signature(Foo.__new__), + ((('cls', ..., ..., "positional_or_keyword"), + ('a', ..., ..., "positional_or_keyword"), + ('b', ..., ..., "positional_or_keyword")), + ...)) + + class Bar: + __new__ = identity(object.__new__) + + object_new_signature = ( + (('args', ..., ..., 'var_positional'), + ('kwargs', ..., ..., 'var_keyword')), + ..., + ) + + self.assertEqual(self.signature(Bar), ((), ...)) + self.assertEqual(self.signature(Bar.__new__), object_new_signature) + self.assertEqual(self.signature(Bar, follow_wrapped=False), + object_new_signature) + self.assertEqual(self.signature(Bar.__new__, follow_wrapped=False), + object_new_signature) + def test_signature_on_class_with_init(self): class C: def __init__(self, b): From c27ee65f595ab3229fa82c301eb3b2e646540174 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sun, 6 Apr 2025 23:38:45 +0800 Subject: [PATCH 17/24] Update news entry --- .../Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst b/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst index dd62d61212d44c..b8b1d12e893c45 100644 --- a/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst +++ b/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst @@ -1,4 +1,4 @@ -Unwrap :meth:`__new__` and :meth:`__init__` methods when getting the class -signature for a class with :func:`inspect.signature`. +Respect follow_wrapped for :func:`__init__` and :func:`__new__` methods when +getting the class signature for a class with :func:`inspect.signature`. Preserve class signature after wrapping with :func:`warnings.deprecated`. Patch by Xuehai Pan. From c148a7d0d50c862484350122b0ad5e6865ee463f Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sun, 6 Apr 2025 23:42:14 +0800 Subject: [PATCH 18/24] Update news entry --- .../next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst b/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst index b8b1d12e893c45..5ecf7e6ab32b79 100644 --- a/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst +++ b/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst @@ -1,4 +1,4 @@ -Respect follow_wrapped for :func:`__init__` and :func:`__new__` methods when +Respect follow_wrapped for :meth:`!__init__` and :meth:`!__new__` methods when getting the class signature for a class with :func:`inspect.signature`. Preserve class signature after wrapping with :func:`warnings.deprecated`. Patch by Xuehai Pan. From 72c1d5aa961a1ac730128b1b11e61606947aa3c0 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sun, 6 Apr 2025 23:42:26 +0800 Subject: [PATCH 19/24] Update news entry --- .../next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst b/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst index 5ecf7e6ab32b79..a1c0fd93f694a4 100644 --- a/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst +++ b/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst @@ -1,4 +1,4 @@ -Respect follow_wrapped for :meth:`!__init__` and :meth:`!__new__` methods when +Respect ``follow_wrapped`` for :meth:`!__init__` and :meth:`!__new__` methods when getting the class signature for a class with :func:`inspect.signature`. Preserve class signature after wrapping with :func:`warnings.deprecated`. Patch by Xuehai Pan. From 2b7445b5090af8aaad346c423f8210da051248b5 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sun, 6 Apr 2025 23:46:29 +0800 Subject: [PATCH 20/24] Update news entry --- .../Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst b/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst index a1c0fd93f694a4..cf8065afc2e5b7 100644 --- a/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst +++ b/Misc/NEWS.d/next/Library/2025-04-03-17-19-42.gh-issue-119605.c7QXAA.rst @@ -1,4 +1,4 @@ -Respect ``follow_wrapped`` for :meth:`!__init__` and :meth:`!__new__` methods when -getting the class signature for a class with :func:`inspect.signature`. +Respect ``follow_wrapped`` for :meth:`!__init__` and :meth:`!__new__` methods +when getting the class signature for a class with :func:`inspect.signature`. Preserve class signature after wrapping with :func:`warnings.deprecated`. Patch by Xuehai Pan. From 2517a21220028712cd872056370e845f2d2b821e Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Mon, 7 Apr 2025 01:21:59 +0800 Subject: [PATCH 21/24] Fix tests --- Lib/inspect.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 95590c9dee457d..0c9331a51c0e9a 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1911,7 +1911,12 @@ def _signature_get_user_defined_method(cls, method_name, *, follow_wrapper_chain else: meth = getattr_static(cls, method_name, None) if meth is not None and follow_wrapper_chains: - meth = unwrap(meth) + unwrapped_meth = unwrap(meth) + if isinstance(meth, (classmethod, staticmethod)): + # Rewrap with the original type + meth = type(meth)(unwrapped_meth) + else: + meth = unwrapped_meth if meth is None or isinstance(meth, _NonUserDefinedCallables): # Once '__signature__' will be added to 'C'-level # callables, this check won't be necessary @@ -2575,8 +2580,7 @@ def _signature_from_callable(obj, *, obj_new = unwrap(obj_new) # We have a class (not metaclass), but no user-defined # __init__ or __new__ for it - if (obj_init is object.__init__ and - obj_new is object.__new__): + if obj_init is object.__init__ and obj_new is object.__new__: # Return a signature of 'object' builtin. return sigcls.from_callable(object) else: From 44f103f126976efd2f20bf4cc317739ffdf95534 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Mon, 7 Apr 2025 01:24:01 +0800 Subject: [PATCH 22/24] Simplify branches --- Lib/inspect.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 0c9331a51c0e9a..35d9a9cd0c4b67 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1910,19 +1910,20 @@ def _signature_get_user_defined_method(cls, method_name, *, follow_wrapper_chain meth = getattr(cls, method_name, None) else: meth = getattr_static(cls, method_name, None) - if meth is not None and follow_wrapper_chains: - unwrapped_meth = unwrap(meth) - if isinstance(meth, (classmethod, staticmethod)): - # Rewrap with the original type - meth = type(meth)(unwrapped_meth) - else: - meth = unwrapped_meth - if meth is None or isinstance(meth, _NonUserDefinedCallables): + if meth is None: + return None + + if follow_wrapper_chains: + meth = unwrap(meth, stop=(lambda m: hasattr(m, "__signature__") + or isinstance(m, (classmethod, staticmethod)))) + if isinstance(meth, _NonUserDefinedCallables): # Once '__signature__' will be added to 'C'-level # callables, this check won't be necessary return None if method_name != '__new__': meth = _descriptor_get(meth, cls) + if follow_wrapper_chains: + meth = unwrap(meth, stop=lambda m: hasattr(m, "__signature__")) return meth From 6693eadcb5248200e5ada354eca5448023892c9f Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Mon, 7 Apr 2025 01:54:30 +0800 Subject: [PATCH 23/24] Handle all method descriptor types --- Lib/inspect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 35d9a9cd0c4b67..9592559ba6dcaa 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1915,7 +1915,7 @@ def _signature_get_user_defined_method(cls, method_name, *, follow_wrapper_chain if follow_wrapper_chains: meth = unwrap(meth, stop=(lambda m: hasattr(m, "__signature__") - or isinstance(m, (classmethod, staticmethod)))) + or _signature_is_builtin(m))) if isinstance(meth, _NonUserDefinedCallables): # Once '__signature__' will be added to 'C'-level # callables, this check won't be necessary From 07368468d7b914a96ae75b5cd7441cbd33e3fce4 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Mon, 14 Apr 2025 18:22:17 +0800 Subject: [PATCH 24/24] Update tests --- Lib/test/test_inspect/test_inspect.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index bfe432eb31baee..8067ddf09db016 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -4021,9 +4021,9 @@ def __init__(self, b): def test_signature_on_class_with_decorated_new(self): def identity(func): @functools.wraps(func) - def wrapper(*args, **kwargs): + def wrapped(*args, **kwargs): return func(*args, **kwargs) - return wrapper + return wrapped class Foo: @identity @@ -4044,18 +4044,18 @@ def __new__(cls, a, b): class Bar: __new__ = identity(object.__new__) - object_new_signature = ( + varargs_signature = ( (('args', ..., ..., 'var_positional'), ('kwargs', ..., ..., 'var_keyword')), ..., ) self.assertEqual(self.signature(Bar), ((), ...)) - self.assertEqual(self.signature(Bar.__new__), object_new_signature) + self.assertEqual(self.signature(Bar.__new__), varargs_signature) self.assertEqual(self.signature(Bar, follow_wrapped=False), - object_new_signature) + varargs_signature) self.assertEqual(self.signature(Bar.__new__, follow_wrapped=False), - object_new_signature) + varargs_signature) def test_signature_on_class_with_init(self): class C: