From 63da7c7b0ca728a41b6269c4678392efb7f26625 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Fri, 31 Jan 2014 14:48:37 -0500 Subject: [PATCH] inspect.signature: Support duck-types of Python functions (Cython, for instance) #17159 --- Doc/whatsnew/3.4.rst | 4 +++ Lib/inspect.py | 32 +++++++++++++++++++-- Lib/test/test_inspect.py | 60 ++++++++++++++++++++++++++++++++++++++++ Misc/NEWS | 3 ++ 4 files changed, 97 insertions(+), 2 deletions(-) diff --git a/Doc/whatsnew/3.4.rst b/Doc/whatsnew/3.4.rst index c9c0da7121e..167cac82b8d 100644 --- a/Doc/whatsnew/3.4.rst +++ b/Doc/whatsnew/3.4.rst @@ -793,6 +793,10 @@ callables that follow ``__signature__`` protocol. It is still recommended to update your code to use :func:`~inspect.signature` directly. (Contributed by Yury Selivanov in :issue:`17481`) +:func:`~inspect.signature` now supports duck types of CPython functions, +which adds support for functions compiled with Cython. (Contributed +by Stefan Behnel and Yury Selivanov in :issue:`17159`) + logging ------- diff --git a/Lib/inspect.py b/Lib/inspect.py index 8de9892afe2..bc7eace9435 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1601,6 +1601,30 @@ def _signature_is_builtin(obj): obj in (type, object)) +def _signature_is_functionlike(obj): + # Internal helper to test if `obj` is a duck type of FunctionType. + # A good example of such objects are functions compiled with + # Cython, which have all attributes that a pure Python function + # would have, but have their code statically compiled. + + if not callable(obj) or isclass(obj): + # All function-like objects are obviously callables, + # and not classes. + return False + + name = getattr(obj, '__name__', None) + code = getattr(obj, '__code__', None) + defaults = getattr(obj, '__defaults__', _void) # Important to use _void ... + kwdefaults = getattr(obj, '__kwdefaults__', _void) # ... and not None here + annotations = getattr(obj, '__annotations__', None) + + return (isinstance(code, types.CodeType) and + isinstance(name, str) and + (defaults is None or isinstance(defaults, tuple)) and + (kwdefaults is None or isinstance(kwdefaults, dict)) and + isinstance(annotations, dict)) + + def _signature_get_bound_param(spec): # Internal helper to get first parameter name from a # __text_signature__ of a builtin method, which should @@ -1670,7 +1694,9 @@ def signature(obj): if _signature_is_builtin(obj): return Signature.from_builtin(obj) - if isinstance(obj, types.FunctionType): + if isfunction(obj) or _signature_is_functionlike(obj): + # If it's a pure Python function, or an object that is duck type + # of a Python function (Cython functions, for instance), then: return Signature.from_function(obj) if isinstance(obj, functools.partial): @@ -2071,7 +2097,9 @@ class Signature: def from_function(cls, func): '''Constructs Signature for the given python function''' - if not isinstance(func, types.FunctionType): + if not (isfunction(func) or _signature_is_functionlike(func)): + # If it's not a pure Python function, and not a duck type + # of pure function: raise TypeError('{!r} is not a Python function'.format(func)) Parameter = cls._parameter_cls diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 477f6016289..e42545de22d 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -1740,6 +1740,66 @@ class TestSignatureObject(unittest.TestCase): with self.assertRaisesRegex(TypeError, 'is not a Python builtin'): inspect.Signature.from_builtin(42) + def test_signature_from_functionlike_object(self): + def func(a,b, *args, kwonly=True, kwonlyreq, **kwargs): + pass + + class funclike: + # Has to be callable, and have correct + # __code__, __annotations__, __defaults__, __name__, + # and __kwdefaults__ attributes + + def __init__(self, func): + self.__name__ = func.__name__ + self.__code__ = func.__code__ + self.__annotations__ = func.__annotations__ + self.__defaults__ = func.__defaults__ + self.__kwdefaults__ = func.__kwdefaults__ + self.func = func + + def __call__(self, *args, **kwargs): + return self.func(*args, **kwargs) + + sig_func = inspect.Signature.from_function(func) + + sig_funclike = inspect.Signature.from_function(funclike(func)) + self.assertEqual(sig_funclike, sig_func) + + sig_funclike = inspect.signature(funclike(func)) + self.assertEqual(sig_funclike, sig_func) + + # If object is not a duck type of function, then + # signature will try to get a signature for its '__call__' + # method + fl = funclike(func) + del fl.__defaults__ + self.assertEqual(self.signature(fl), + ((('args', ..., ..., "var_positional"), + ('kwargs', ..., ..., "var_keyword")), + ...)) + + def test_signature_functionlike_class(self): + # We only want to duck type function-like objects, + # not classes. + + def func(a,b, *args, kwonly=True, kwonlyreq, **kwargs): + pass + + class funclike: + def __init__(self, marker): + pass + + __name__ = func.__name__ + __code__ = func.__code__ + __annotations__ = func.__annotations__ + __defaults__ = func.__defaults__ + __kwdefaults__ = func.__kwdefaults__ + + with self.assertRaisesRegex(TypeError, 'is not a Python function'): + inspect.Signature.from_function(funclike) + + self.assertEqual(str(inspect.signature(funclike)), '(marker)') + def test_signature_on_method(self): class Test: def __init__(*args): diff --git a/Misc/NEWS b/Misc/NEWS index cdd8209ff74..d4fb7210b69 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -13,6 +13,9 @@ Core and Builtins Library ------- +- Issue #17159: inspect.signature now accepts duck types of functions, + which adds support for Cython functions. Initial patch by Stefan Behnel. + - Issue #18801: Fix inspect.classify_class_attrs to correctly classify object.__new__ and object.__init__.