summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorYury Selivanov <yselivanov@sprymix.com>2014-01-31 19:48:37 (GMT)
committerYury Selivanov <yselivanov@sprymix.com>2014-01-31 19:48:37 (GMT)
commit63da7c7b0ca728a41b6269c4678392efb7f26625 (patch)
tree0e6f6d57448161a2faf9d3b00a40c57664432086 /Lib
parent4ded1f35532b7da37df2bba37a7ad32334349270 (diff)
downloadcpython-63da7c7b0ca728a41b6269c4678392efb7f26625.zip
cpython-63da7c7b0ca728a41b6269c4678392efb7f26625.tar.gz
cpython-63da7c7b0ca728a41b6269c4678392efb7f26625.tar.bz2
inspect.signature: Support duck-types of Python functions (Cython, for instance) #17159
Diffstat (limited to 'Lib')
-rw-r--r--Lib/inspect.py32
-rw-r--r--Lib/test/test_inspect.py60
2 files changed, 90 insertions, 2 deletions
diff --git a/Lib/inspect.py b/Lib/inspect.py
index 8de9892..bc7eace 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 477f601..e42545d 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):