From e155934a941e744d40ac41b8860c138d72d8c5f8 Mon Sep 17 00:00:00 2001 From: Heckad Date: Thu, 9 Mar 2023 21:35:40 +0300 Subject: [PATCH 1/5] gh-101293: Fix support of custom callables and types in inspect.Signature.from_callable() Support callables with the __call__() method and types with __new__() and __init__() methods set to class methods, static methods, bound methods, partial functions, and other types of methods and descriptors. Add tests for numerous types of callables and descriptors. --- Lib/inspect.py | 39 +- Lib/test/test_inspect/test_inspect.py | 363 +++++++++++++++++- ...-02-15-19-11-49.gh-issue-101293.898b8l.rst | 4 + 3 files changed, 387 insertions(+), 19 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-02-15-19-11-49.gh-issue-101293.898b8l.rst diff --git a/Lib/inspect.py b/Lib/inspect.py index f0b72662a9a0b2..d1ee9f667a4002 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2495,6 +2495,15 @@ def _signature_from_function(cls, func, skip_bound_arg=True, __validate_parameters__=is_duck_function) +def _descriptor_get(descriptor, obj): + if isclass(descriptor): + return descriptor + get = getattr(type(descriptor), '__get__', _sentinel) + if get is _sentinel: + return descriptor + return get(descriptor, obj, type(obj)) + + def _signature_from_callable(obj, *, follow_wrapper_chains=True, skip_bound_arg=True, @@ -2609,28 +2618,27 @@ 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__') - if call is not None: - sig = _get_signature_of(call) + call = getattr_static(type(obj), '__call__', None) + if call is not None and not isinstance(call, _NonUserDefinedCallables): + call = _descriptor_get(call, obj) + return _get_signature_of(call) else: - factory_method = None new = _signature_get_user_defined_method(obj, '__new__') - init = _signature_get_user_defined_method(obj, '__init__') + init = getattr_static(obj, '__init__', None) + if isinstance(init, _NonUserDefinedCallables): + init = None # 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__: - factory_method = new + sig = _get_signature_of(new) break # or an own '__init__' method elif init is not None and '__init__' in base.__dict__: - factory_method = init - break - - if factory_method is not None: - sig = _get_signature_of(factory_method) + init = _descriptor_get(init, obj) + return _get_signature_of(init) if sig is None: # At this point we know, that `obj` is a class, with no user- @@ -2673,13 +2681,10 @@ def _signature_from_callable(obj, *, # We also check that the 'obj' is not an instance of # types.WrapperDescriptorType or types.MethodWrapperType to avoid # infinite recursion (and even potential segfault) - call = _signature_get_user_defined_method(type(obj), '__call__') + call = getattr_static(type(obj), '__call__', None) if call is not None: - try: - sig = _get_signature_of(call) - except ValueError as ex: - msg = 'no signature found for {!r}'.format(obj) - raise ValueError(msg) from ex + call = _descriptor_get(call, obj) + return _get_signature_of(call) if sig is not None: # For classes and objects we skip the first parameter of their diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 4611f62b293ff9..efbeff97bd15f9 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -2899,9 +2899,12 @@ def p(name): return signature.parameters[name].default # This doesn't work now. # (We don't have a valid signature for "type" in 3.4) + class ThisWorksNow: + __call__ = type + # TODO: Support type. + self.assertEqual(ThisWorksNow()(1), int) + self.assertEqual(ThisWorksNow()('A', (), {}).__name__, 'A') with self.assertRaisesRegex(ValueError, "no signature found"): - class ThisWorksNow: - __call__ = type test_callable(ThisWorksNow()) # Regression test for issue #20786 @@ -3484,6 +3487,98 @@ def __init__(self, b): ((('a', ..., ..., "positional_or_keyword"),), ...)) + with self.subTest('classmethod'): + class CM(type): + @classmethod + def __call__(cls, a): + return a + class C(metaclass=CM): + def __init__(self, b): + pass + + self.assertEqual(C(1), 1) + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('staticmethod'): + class CM(type): + @staticmethod + def __call__(a): + return a + class C(metaclass=CM): + def __init__(self, b): + pass + + self.assertEqual(C(1), 1) + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('MethodType'): + class A: + def call(self, a): + return a + class CM(type): + __call__ = A().call + class C(metaclass=CM): + def __init__(self, b): + pass + + self.assertEqual(C(1), 1) + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('partial'): + class CM(type): + __call__ = functools.partial(lambda x, a: (x, a), 2) + class C(metaclass=CM): + def __init__(self, b): + pass + + self.assertEqual(C(1), (2, 1)) + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('partialmethod'): + class CM(type): + __call__ = functools.partialmethod(lambda self, x, a: (x, a), 2) + class C(metaclass=CM): + def __init__(self, b): + pass + + self.assertEqual(C(1), (2, 1)) + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('BuiltinMethodType'): + class CM(type): + __call__ = ':'.join + class C(metaclass=CM): + def __init__(self, b): + pass + + self.assertEqual(C(['a', 'bc']), 'a:bc') + # BUG: Returns '' + with self.assertRaises(AssertionError): + self.assertEqual(self.signature(C), self.signature(''.join)) + + with self.subTest('MethodWrapperType'): + class CM(type): + __call__ = (2).__pow__ + class C(metaclass=CM): + def __init__(self, b): + pass + + self.assertEqual(C(3), 8) + self.assertEqual(C(3, 7), 1) + # BUG: Returns '' + with self.assertRaises(AssertionError): + self.assertEqual(self.signature(C), self.signature((0).__pow__)) + class CM(type): def __new__(mcls, name, bases, dct, *, foo=1): return super().__new__(mcls, name, bases, dct) @@ -3545,6 +3640,169 @@ def __init__(self, b): ('bar', 2, ..., "keyword_only")), ...)) + def test_signature_on_class_with_init(self): + class C: + def __init__(self, b): + pass + + C(1) + self.assertEqual(self.signature(C), + ((('b', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('classmethod'): + class C: + @classmethod + def __init__(cls, b): + pass + + C(1) + self.assertEqual(self.signature(C), + ((('b', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('staticmethod'): + class C: + @staticmethod + def __init__(b): + pass + + C(1) + self.assertEqual(self.signature(C), + ((('b', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('MethodType'): + class A: + def call(self, a): + return + class C: + __init__ = A().call + + C(1) + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('partial'): + class C: + __init__ = functools.partial(lambda x, a: None, 2) + + C(1) + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('partialmethod'): + class C: + def _init(self, x, a): + self.a = (x, a) + __init__ = functools.partialmethod(_init, 2) + + self.assertEqual(C(1).a, (2, 1)) + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + def test_signature_on_class_with_new(self): + with self.subTest('FunctionType'): + class C: + def __new__(cls, a): + return a + + self.assertEqual(C(1), 1) + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('classmethod'): + class C: + @classmethod + def __new__(cls, cls2, a): + return a + + self.assertEqual(C(1), 1) + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('staticmethod'): + class C: + @staticmethod + def __new__(cls, a): + return a + + self.assertEqual(C(1), 1) + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('MethodType'): + class A: + def call(self, cls, a): + return a + class C: + __new__ = A().call + + self.assertEqual(C(1), 1) + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('partial'): + class C: + __new__ = functools.partial(lambda x, cls, a: (x, a), 2) + + self.assertEqual(C(1), (2, 1)) + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('partialmethod'): + class C: + __new__ = functools.partialmethod(lambda cls, x, a: (x, a), 2) + + self.assertEqual(C(1), (2, 1)) + self.assertEqual(self.signature(C), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('BuiltinMethodType'): + class C: + __new__ = str.__subclasscheck__ + + self.assertEqual(C(), False) + # TODO: Support BuiltinMethodType + # self.assertEqual(self.signature(C), ((), ...)) + self.assertRaises(ValueError, self.signature, C) + + with self.subTest('MethodWrapperType'): + class C: + __new__ = type.__or__.__get__(int, type) + + self.assertEqual(C(), C | int) + # TODO: Support MethodWrapperType + # self.assertEqual(self.signature(C), ((), ...)) + self.assertRaises(ValueError, self.signature, C) + + # TODO: Test ClassMethodDescriptorType + + with self.subTest('MethodDescriptorType'): + class C: + __new__ = type.__dict__['__subclasscheck__'] + + self.assertEqual(C(C), True) + self.assertEqual(self.signature(C), self.signature(C.__subclasscheck__)) + + with self.subTest('WrapperDescriptorType'): + class C: + __new__ = type.__or__ + + self.assertEqual(C(int), C | int) + # TODO: Support WrapperDescriptorType + # self.assertEqual(self.signature(C), self.signature(C.__or__)) + self.assertRaises(ValueError, self.signature, C) + def test_signature_on_subclass(self): class A: def __new__(cls, a=1, *args, **kwargs): @@ -3598,8 +3856,11 @@ class D(C): pass # Test meta-classes without user-defined __init__ or __new__ class C(type): pass class D(C): pass + C('A', (), {}) + # TODO: Support type. with self.assertRaisesRegex(ValueError, "callable.*is not supported"): self.assertEqual(inspect.signature(C), None) + D('A', (), {}) with self.assertRaisesRegex(ValueError, "callable.*is not supported"): self.assertEqual(inspect.signature(D), None) @@ -3649,6 +3910,104 @@ class Bar(Spam, Foo): ((('a', ..., ..., "positional_or_keyword"),), ...)) + with self.subTest('classmethod'): + class C: + @classmethod + def __call__(cls, a): + pass + + self.assertEqual(self.signature(C()), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('staticmethod'): + class C: + @staticmethod + def __call__(a): + pass + + self.assertEqual(self.signature(C()), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('MethodType'): + class A: + def call(self, a): + return a + class C: + __call__ = A().call + + self.assertEqual(C()(1), 1) + self.assertEqual(self.signature(C()), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('partial'): + class C: + __call__ = functools.partial(lambda x, a: (x, a), 2) + + self.assertEqual(C()(1), (2, 1)) + self.assertEqual(self.signature(C()), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('partialmethod'): + class C: + __call__ = functools.partialmethod(lambda self, x, a: (x, a), 2) + + self.assertEqual(C()(1), (2, 1)) + self.assertEqual(self.signature(C()), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + with self.subTest('BuiltinMethodType'): + class C: + __call__ = ':'.join + + self.assertEqual(C()(['a', 'bc']), 'a:bc') + self.assertEqual(self.signature(C()), self.signature(''.join)) + + with self.subTest('MethodWrapperType'): + class C: + __call__ = (2).__pow__ + + self.assertEqual(C()(3), 8) + self.assertEqual(self.signature(C()), self.signature((0).__pow__)) + + with self.subTest('ClassMethodDescriptorType'): + class C(dict): + __call__ = dict.__dict__['fromkeys'] + + res = C()([1, 2], 3) + self.assertEqual(res, {1: 3, 2: 3}) + self.assertEqual(type(res), C) + self.assertEqual(self.signature(C()), self.signature(dict.fromkeys)) + + with self.subTest('MethodDescriptorType'): + class C(str): + __call__ = str.join + + self.assertEqual(C(':')(['a', 'bc']), 'a:bc') + self.assertEqual(self.signature(C()), self.signature(''.join)) + + with self.subTest('WrapperDescriptorType'): + class C(int): + __call__ = int.__pow__ + + self.assertEqual(C(2)(3), 8) + self.assertEqual(self.signature(C()), self.signature((0).__pow__)) + + with self.subTest('MemberDescriptorType'): + class C: + __slots__ = '__call__' + c = C() + c.__call__ = lambda a: a + self.assertEqual(c(1), 1) + self.assertEqual(self.signature(c), + ((('a', ..., ..., "positional_or_keyword"),), + ...)) + + def test_signature_on_wrapped(self): class Wrapped: pass Wrapped.__wrapped__ = lambda a: None diff --git a/Misc/NEWS.d/next/Library/2024-02-15-19-11-49.gh-issue-101293.898b8l.rst b/Misc/NEWS.d/next/Library/2024-02-15-19-11-49.gh-issue-101293.898b8l.rst new file mode 100644 index 00000000000000..98365d2edbc4b5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-02-15-19-11-49.gh-issue-101293.898b8l.rst @@ -0,0 +1,4 @@ +Support callables with the ``__call__()`` method and types with +``__new__()`` and ``__init__()`` methods set to class methods, static +methods, bound methods, partial functions, and other types of methods and +descriptors in :meth:`inspect.Signature.from_callable`. From 556119f4ada01cfce8154748d42f8f02d20fa8c0 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 15 Feb 2024 20:34:06 +0200 Subject: [PATCH 2/5] Support __wrapped__. --- Lib/inspect.py | 3 ++- Lib/test/test_inspect/test_inspect.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index d1ee9f667a4002..a474ef4088e574 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2542,7 +2542,8 @@ def _signature_from_callable(obj, *, # Unwrap until we find an explicit signature or a MethodType (which will be # handled explicitly below). obj = unwrap(obj, stop=(lambda f: hasattr(f, "__signature__") - or isinstance(f, types.MethodType))) + or isinstance(f, types.MethodType) + or not callable(f.__wrapped__))) if isinstance(obj, types.MethodType): # If the unwrapped object is a *method*, we might want to # skip its first parameter (self). diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index efbeff97bd15f9..6bef486adbecef 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -3111,6 +3111,10 @@ def m1d(*args, **kwargs): int)) def test_signature_on_classmethod(self): + self.assertEqual(self.signature(classmethod), + ((('function', ..., ..., "positional_only"),), + ...)) + class Test: @classmethod def foo(cls, arg1, *, arg2=1): @@ -3129,6 +3133,10 @@ def foo(cls, arg1, *, arg2=1): ...)) def test_signature_on_staticmethod(self): + self.assertEqual(self.signature(staticmethod), + ((('function', ..., ..., "positional_only"),), + ...)) + class Test: @staticmethod def foo(cls, *, arg): From d69c993fa5496cbc345ce5ac734c6ec567d172b0 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 16 Feb 2024 12:09:57 +0200 Subject: [PATCH 3/5] Add some comments. --- Lib/inspect.py | 2 ++ Lib/test/test_inspect/test_inspect.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index a474ef4088e574..d7b1fa755e87ce 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2543,6 +2543,8 @@ def _signature_from_callable(obj, *, # handled explicitly below). obj = unwrap(obj, stop=(lambda f: hasattr(f, "__signature__") or isinstance(f, types.MethodType) + # it can be a non-callable data descriptor + # like staticmethod.__wrapped__ or not callable(f.__wrapped__))) if isinstance(obj, types.MethodType): # If the unwrapped object is a *method*, we might want to diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 6bef486adbecef..30018efdf957fa 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -3653,7 +3653,7 @@ class C: def __init__(self, b): pass - C(1) + C(1) # does not raise self.assertEqual(self.signature(C), ((('b', ..., ..., "positional_or_keyword"),), ...)) @@ -3664,7 +3664,7 @@ class C: def __init__(cls, b): pass - C(1) + C(1) # does not raise self.assertEqual(self.signature(C), ((('b', ..., ..., "positional_or_keyword"),), ...)) @@ -3675,7 +3675,7 @@ class C: def __init__(b): pass - C(1) + C(1) # does not raise self.assertEqual(self.signature(C), ((('b', ..., ..., "positional_or_keyword"),), ...)) @@ -3683,11 +3683,11 @@ def __init__(b): with self.subTest('MethodType'): class A: def call(self, a): - return + pass class C: __init__ = A().call - C(1) + C(1) # does not raise self.assertEqual(self.signature(C), ((('a', ..., ..., "positional_or_keyword"),), ...)) @@ -3696,7 +3696,7 @@ class C: class C: __init__ = functools.partial(lambda x, a: None, 2) - C(1) + C(1) # does not raise self.assertEqual(self.signature(C), ((('a', ..., ..., "positional_or_keyword"),), ...)) @@ -3864,11 +3864,11 @@ class D(C): pass # Test meta-classes without user-defined __init__ or __new__ class C(type): pass class D(C): pass - C('A', (), {}) + self.assertEqual(C('A', (), {}).__name__, 'A') # TODO: Support type. with self.assertRaisesRegex(ValueError, "callable.*is not supported"): self.assertEqual(inspect.signature(C), None) - D('A', (), {}) + self.assertEqual(D('A', (), {}).__name__, 'A') with self.assertRaisesRegex(ValueError, "callable.*is not supported"): self.assertEqual(inspect.signature(D), None) From 5670730e76067a425cea8cbaf1502a1c48d011f1 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 26 Feb 2024 20:14:58 +0200 Subject: [PATCH 4/5] Remove an unneeded workaround for wrappers. --- Lib/inspect.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 2076d83faabaff..aba3a378b07458 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2539,10 +2539,7 @@ def _signature_from_callable(obj, *, # Unwrap until we find an explicit signature or a MethodType (which will be # handled explicitly below). obj = unwrap(obj, stop=(lambda f: hasattr(f, "__signature__") - or isinstance(f, types.MethodType) - # it can be a non-callable data descriptor - # like staticmethod.__wrapped__ - or not callable(f.__wrapped__))) + or isinstance(f, types.MethodType))) if isinstance(obj, types.MethodType): # If the unwrapped object is a *method*, we might want to # skip its first parameter (self). From 8d1515b15f09e2f806a6b51345e21e27658caa60 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 27 Feb 2024 10:27:54 +0200 Subject: [PATCH 5/5] Implement suggestions from review. --- Lib/inspect.py | 146 ++++++++++++++++++++++--------------------------- 1 file changed, 64 insertions(+), 82 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index aba3a378b07458..8a2b2c96e993b5 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2039,15 +2039,17 @@ def _signature_get_user_defined_method(cls, method_name): named ``method_name`` and returns it only if it is a pure python function. """ - try: - meth = getattr(cls, method_name) - except AttributeError: - return + if method_name == '__new__': + meth = getattr(cls, method_name, None) else: - if not isinstance(meth, _NonUserDefinedCallables): - # Once '__signature__' will be added to 'C'-level - # callables, this check won't be necessary - return meth + meth = getattr_static(cls, method_name, None) + 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 + if method_name != '__new__': + meth = _descriptor_get(meth, cls) + return meth def _signature_get_partial(wrapped_sig, partial, extra_args=()): @@ -2609,93 +2611,73 @@ def _signature_from_callable(obj, *, wrapped_sig = _get_signature_of(obj.func) return _signature_get_partial(wrapped_sig, obj) - sig = None if isinstance(obj, type): # obj is a class or a metaclass # First, let's see if it has an overloaded __call__ defined # in its metaclass - call = getattr_static(type(obj), '__call__', None) - if call is not None and not isinstance(call, _NonUserDefinedCallables): - call = _descriptor_get(call, obj) + call = _signature_get_user_defined_method(type(obj), '__call__') + if call is not None: return _get_signature_of(call) - else: - new = _signature_get_user_defined_method(obj, '__new__') - init = getattr_static(obj, '__init__', None) - if isinstance(init, _NonUserDefinedCallables): - init = None - - # 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) - break - # or an own '__init__' method - elif init is not None and '__init__' in base.__dict__: - init = _descriptor_get(init, obj) - return _get_signature_of(init) - - if sig is None: - # At this point we know, that `obj` is a class, with no user- - # defined '__init__', '__new__', or class-level '__call__' - - for base in obj.__mro__[:-1]: - # Since '__text_signature__' is implemented as a - # descriptor that extracts text signature from the - # class docstring, if 'obj' is derived from a builtin - # class, its own '__text_signature__' may be 'None'. - # Therefore, we go through the MRO (except the last - # class in there, which is 'object') to find the first - # class with non-empty text signature. - try: - text_sig = base.__text_signature__ - except AttributeError: - pass - else: - if text_sig: - # If 'base' class has a __text_signature__ attribute: - # return a signature based on it - return _signature_fromstr(sigcls, base, text_sig) - - # No '__text_signature__' was found for the 'obj' class. - # Last option is to check if its '__init__' is - # object.__init__ or type.__init__. - 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__): - # Return a signature of 'object' builtin. - return sigcls.from_callable(object) - else: - raise ValueError( - 'no signature found for builtin type {!r}'.format(obj)) - elif not isinstance(obj, _NonUserDefinedCallables): + 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 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 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__' + + for base in obj.__mro__[:-1]: + # Since '__text_signature__' is implemented as a + # descriptor that extracts text signature from the + # class docstring, if 'obj' is derived from a builtin + # class, its own '__text_signature__' may be 'None'. + # Therefore, we go through the MRO (except the last + # class in there, which is 'object') to find the first + # class with non-empty text signature. + try: + text_sig = base.__text_signature__ + except AttributeError: + pass + else: + if text_sig: + # If 'base' class has a __text_signature__ attribute: + # return a signature based on it + return _signature_fromstr(sigcls, base, text_sig) + + # No '__text_signature__' was found for the 'obj' class. + # Last option is to check if its '__init__' is + # object.__init__ or type.__init__. + 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__): + # Return a signature of 'object' builtin. + return sigcls.from_callable(object) + else: + raise ValueError( + 'no signature found for builtin type {!r}'.format(obj)) + + else: # An object with __call__ - # We also check that the 'obj' is not an instance of - # types.WrapperDescriptorType or types.MethodWrapperType to avoid - # infinite recursion (and even potential segfault) call = getattr_static(type(obj), '__call__', None) if call is not None: call = _descriptor_get(call, obj) return _get_signature_of(call) - if sig is not None: - # For classes and objects we skip the first parameter of their - # __call__, __new__, or __init__ methods - if skip_bound_arg: - return _signature_bound_method(sig) - else: - return sig - - if isinstance(obj, types.BuiltinFunctionType): - # Raise a nicer error message for builtins - msg = 'no signature found for builtin function {!r}'.format(obj) - raise ValueError(msg) - raise ValueError('callable {!r} is not supported by signature'.format(obj))