Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
aa8865e
Preserve class signature after wrapping with `@warnings.deprecated`
XuehaiPan Jun 3, 2024
d093b57
📜🤖 Added by blurb_it.
blurb-it[bot] Apr 3, 2025
9664211
Add test to class signature
XuehaiPan Apr 3, 2025
a1dd52e
Fix tests
XuehaiPan Apr 3, 2025
668adb3
Merge branch 'main' into deprecated-signature
XuehaiPan Apr 3, 2025
68eebad
Handle potential assignment failure
XuehaiPan Apr 3, 2025
57c61c3
Update tests
XuehaiPan Apr 4, 2025
6e28f0b
Use `__signature__`
XuehaiPan Apr 4, 2025
88666b0
Merge branch 'main' into deprecated-signature
XuehaiPan Apr 4, 2025
43b0977
Remove duplicate in tests
XuehaiPan Apr 4, 2025
4dd8b70
Unwrap decorated __new__ and __init__ in inspect
XuehaiPan Apr 4, 2025
6b7bd71
Unwrap decorated __new__ and __init__ in inspect
XuehaiPan Apr 4, 2025
bb2c39b
Merge branch 'main' into deprecated-signature
XuehaiPan Apr 4, 2025
ca8aa96
Fix tests
XuehaiPan Apr 4, 2025
636c687
Fix tests
XuehaiPan Apr 4, 2025
97a17b2
Merge branch 'main' into deprecated-signature
XuehaiPan Apr 4, 2025
930881f
Merge branch 'main' into deprecated-signature
XuehaiPan Apr 5, 2025
11f8218
Merge branch 'main' into deprecated-signature
XuehaiPan Apr 6, 2025
198f8ee
Revert unneeded changes
XuehaiPan Apr 6, 2025
2c50424
Respect `follow_wrapper_chains`
XuehaiPan Apr 6, 2025
c62184a
Update news entry
XuehaiPan Apr 6, 2025
759d403
Add test for inspect
XuehaiPan Apr 6, 2025
c27ee65
Update news entry
XuehaiPan Apr 6, 2025
c148a7d
Update news entry
XuehaiPan Apr 6, 2025
72c1d5a
Update news entry
XuehaiPan Apr 6, 2025
2b7445b
Update news entry
XuehaiPan Apr 6, 2025
e8a3a1e
Merge branch 'main' into deprecated-signature
XuehaiPan Apr 6, 2025
2517a21
Fix tests
XuehaiPan Apr 6, 2025
44f103f
Simplify branches
XuehaiPan Apr 6, 2025
6693ead
Handle all method descriptor types
XuehaiPan Apr 6, 2025
82ab31e
Merge branch 'main' into deprecated-signature
XuehaiPan Apr 7, 2025
9386d40
Merge branch 'main' into deprecated-signature
XuehaiPan Apr 11, 2025
8627478
Merge branch 'main' into deprecated-signature
XuehaiPan Apr 13, 2025
0736846
Update tests
XuehaiPan Apr 14, 2025
2a76c9e
Merge branch 'main' into deprecated-signature
XuehaiPan Apr 16, 2025
144e629
Merge branch 'main' into deprecated-signature
XuehaiPan May 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1910,7 +1910,7 @@ 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(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
Expand Down Expand Up @@ -2511,6 +2511,8 @@ def _signature_from_callable(obj, *,
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__')

Expand Down Expand Up @@ -2554,8 +2556,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:
Expand Down
60 changes: 60 additions & 0 deletions Lib/test/test_warnings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1832,10 +1832,70 @@ 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

# The `@deprecated` decorator will update the class in-place.
# Test the child classes first.
for cls in reversed((Cls1, Cls2, Cls3, Cls4, Cls5, Cls6, Cls7, Cls8)):
with self.subTest(f'class {cls.__name__} signature'):
try:
original_signature = inspect.signature(cls)
except ValueError:
original_signature = None
try:
original_new_signature = inspect.signature(cls.__new__)
except ValueError:
original_new_signature = None

deprecated_cls = deprecated("depr")(cls)

try:
deprecated_signature = inspect.signature(deprecated_cls)
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_new_signature, deprecated_new_signature)


def setUpModule():
py_warnings.onceregistry.clear()
c_warnings.onceregistry.clear()


tearDownModule = setUpModule

if __name__ == "__main__":
Expand Down
7 changes: 5 additions & 2 deletions Lib/warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,17 +590,19 @@ def __call__(self, arg, /):
if category is None:
arg.__deprecated__ = msg
return arg
elif isinstance(arg, type):

if 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 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):
Expand All @@ -622,6 +624,7 @@ 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:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Preserve class signature after wrapping with :func:`warnings.deprecated`. Patch by Xuehai Pan.
Loading