gh-119605: Respect follow_wrapped
for __init__
and __new__
when getting class signature with inspect.signature
(#132055)
This commit is contained in:
parent
c14134020f
commit
b8633f9aca
@ -1901,7 +1901,7 @@ _NonUserDefinedCallables = (types.WrapperDescriptorType,
|
|||||||
types.BuiltinFunctionType)
|
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
|
"""Private helper. Checks if ``cls`` has an attribute
|
||||||
named ``method_name`` and returns it only if it is a
|
named ``method_name`` and returns it only if it is a
|
||||||
pure python function.
|
pure python function.
|
||||||
@ -1910,12 +1910,20 @@ def _signature_get_user_defined_method(cls, method_name):
|
|||||||
meth = getattr(cls, method_name, None)
|
meth = getattr(cls, method_name, None)
|
||||||
else:
|
else:
|
||||||
meth = getattr_static(cls, method_name, None)
|
meth = getattr_static(cls, method_name, None)
|
||||||
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 _signature_is_builtin(m)))
|
||||||
|
if isinstance(meth, _NonUserDefinedCallables):
|
||||||
# Once '__signature__' will be added to 'C'-level
|
# Once '__signature__' will be added to 'C'-level
|
||||||
# callables, this check won't be necessary
|
# callables, this check won't be necessary
|
||||||
return None
|
return None
|
||||||
if method_name != '__new__':
|
if method_name != '__new__':
|
||||||
meth = _descriptor_get(meth, cls)
|
meth = _descriptor_get(meth, cls)
|
||||||
|
if follow_wrapper_chains:
|
||||||
|
meth = unwrap(meth, stop=lambda m: hasattr(m, "__signature__"))
|
||||||
return meth
|
return meth
|
||||||
|
|
||||||
|
|
||||||
@ -2507,12 +2515,26 @@ def _signature_from_callable(obj, *,
|
|||||||
|
|
||||||
# First, let's see if it has an overloaded __call__ defined
|
# First, let's see if it has an overloaded __call__ defined
|
||||||
# in its metaclass
|
# 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:
|
if call is not None:
|
||||||
return _get_signature_of(call)
|
return _get_signature_of(call)
|
||||||
|
|
||||||
new = _signature_get_user_defined_method(obj, '__new__')
|
# NOTE: The user-defined method can be a function with a thin wrapper
|
||||||
init = _signature_get_user_defined_method(obj, '__init__')
|
# around object.__new__ (e.g., generated by `@warnings.deprecated`)
|
||||||
|
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
|
# Go through the MRO and see if any class has user-defined
|
||||||
# pure Python __new__ or __init__ method
|
# pure Python __new__ or __init__ method
|
||||||
@ -2552,10 +2574,14 @@ def _signature_from_callable(obj, *,
|
|||||||
# Last option is to check if its '__init__' is
|
# Last option is to check if its '__init__' is
|
||||||
# object.__init__ or type.__init__.
|
# object.__init__ or type.__init__.
|
||||||
if type not in obj.__mro__:
|
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
|
# We have a class (not metaclass), but no user-defined
|
||||||
# __init__ or __new__ for it
|
# __init__ or __new__ for it
|
||||||
if (obj.__init__ is object.__init__ and
|
if obj_init is object.__init__ and obj_new is object.__new__:
|
||||||
obj.__new__ is object.__new__):
|
|
||||||
# Return a signature of 'object' builtin.
|
# Return a signature of 'object' builtin.
|
||||||
return sigcls.from_callable(object)
|
return sigcls.from_callable(object)
|
||||||
else:
|
else:
|
||||||
|
@ -3847,7 +3847,6 @@ class TestSignatureObject(unittest.TestCase):
|
|||||||
('b', ..., ..., "positional_or_keyword")),
|
('b', ..., ..., "positional_or_keyword")),
|
||||||
...))
|
...))
|
||||||
|
|
||||||
|
|
||||||
def test_signature_on_class(self):
|
def test_signature_on_class(self):
|
||||||
class C:
|
class C:
|
||||||
def __init__(self, a):
|
def __init__(self, a):
|
||||||
@ -4022,6 +4021,45 @@ class TestSignatureObject(unittest.TestCase):
|
|||||||
('bar', 2, ..., "keyword_only")),
|
('bar', 2, ..., "keyword_only")),
|
||||||
...))
|
...))
|
||||||
|
|
||||||
|
def test_signature_on_class_with_decorated_new(self):
|
||||||
|
def identity(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
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__)
|
||||||
|
|
||||||
|
varargs_signature = (
|
||||||
|
(('args', ..., ..., 'var_positional'),
|
||||||
|
('kwargs', ..., ..., 'var_keyword')),
|
||||||
|
...,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(self.signature(Bar), ((), ...))
|
||||||
|
self.assertEqual(self.signature(Bar.__new__), varargs_signature)
|
||||||
|
self.assertEqual(self.signature(Bar, follow_wrapped=False),
|
||||||
|
varargs_signature)
|
||||||
|
self.assertEqual(self.signature(Bar.__new__, follow_wrapped=False),
|
||||||
|
varargs_signature)
|
||||||
|
|
||||||
def test_signature_on_class_with_init(self):
|
def test_signature_on_class_with_init(self):
|
||||||
class C:
|
class C:
|
||||||
def __init__(self, b):
|
def __init__(self, b):
|
||||||
|
@ -2018,10 +2018,70 @@ class DeprecatedTests(PyPublicAPITests):
|
|||||||
self.assertFalse(inspect.iscoroutinefunction(Cls.sync))
|
self.assertFalse(inspect.iscoroutinefunction(Cls.sync))
|
||||||
self.assertTrue(inspect.iscoroutinefunction(Cls.coro))
|
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():
|
def setUpModule():
|
||||||
py_warnings.onceregistry.clear()
|
py_warnings.onceregistry.clear()
|
||||||
c_warnings.onceregistry.clear()
|
c_warnings.onceregistry.clear()
|
||||||
|
|
||||||
|
|
||||||
tearDownModule = setUpModule
|
tearDownModule = setUpModule
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
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.
|
Loading…
x
Reference in New Issue
Block a user