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.
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:
+ 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
# 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
# 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)
- 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`)
+ 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
# 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 (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:
('b', ..., ..., "positional_or_keyword")),
...))
-
def test_signature_on_class(self):
class C:
def __init__(self, a):
('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):
class C:
def __init__(self, b):
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__":
--- /dev/null
+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.