inspect.getfullargspec: Use inspect.signature API behind the scenes #17481
authorYury Selivanov <yselivanov@sprymix.com>
Wed, 29 Jan 2014 16:24:39 +0000 (11:24 -0500)
committerYury Selivanov <yselivanov@sprymix.com>
Wed, 29 Jan 2014 16:24:39 +0000 (11:24 -0500)
Doc/whatsnew/3.4.rst
Lib/inspect.py
Lib/test/test_inspect.py
Misc/NEWS

index 47183391000712cf00de0989793d2c9fdcae146c..d7a04f65d62bde00ab577898abb81fb4d63ef765 100644 (file)
@@ -786,6 +786,13 @@ As part of the implementation of the new :mod:`enum` module, the
 metaclasses (Contributed by Ethan Furman in :issue:`18929` and
 :issue:`19030`)
 
+:func:`~inspect.getfullargspec` and :func:`~inspect.getargspec`
+now use the :func:`~inspect.signature` API. This allows them to
+support much broader range of functions, including some builtins and
+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`)
+
 
 logging
 -------
index f06138c7a880e7f7044d25dd8202dc0fd318d030..4f9c1d1719cdd320f7178b3a8e0c59cbff55a227 100644 (file)
@@ -934,7 +934,7 @@ FullArgSpec = namedtuple('FullArgSpec',
     'args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations')
 
 def getfullargspec(func):
-    """Get the names and default values of a function's arguments.
+    """Get the names and default values of a callable object's arguments.
 
     A tuple of seven things is returned:
     (args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults annotations).
@@ -948,13 +948,90 @@ def getfullargspec(func):
     The first four items in the tuple correspond to getargspec().
     """
 
+    builtin_method_param = None
+
     if ismethod(func):
+        # There is a notable difference in behaviour between getfullargspec
+        # and Signature: the former always returns 'self' parameter for bound
+        # methods, whereas the Signature always shows the actual calling
+        # signature of the passed object.
+        #
+        # To simulate this behaviour, we "unbind" bound methods, to trick
+        # inspect.signature to always return their first parameter ("self",
+        # usually)
         func = func.__func__
-    if not isfunction(func):
-        raise TypeError('{!r} is not a Python function'.format(func))
-    args, varargs, kwonlyargs, varkw = _getfullargs(func.__code__)
-    return FullArgSpec(args, varargs, varkw, func.__defaults__,
-            kwonlyargs, func.__kwdefaults__, func.__annotations__)
+
+    elif isbuiltin(func):
+        # We have a builtin function or method. For that, we check the
+        # special '__text_signature__' attribute, provided by the
+        # Argument Clinic. If it's a method, we'll need to make sure
+        # that its first parameter (usually "self") is always returned
+        # (see the previous comment).
+        text_signature = getattr(func, '__text_signature__', None)
+        if text_signature and text_signature.startswith('($'):
+            builtin_method_param = _signature_get_bound_param(text_signature)
+
+    try:
+        sig = signature(func)
+    except Exception as ex:
+        # Most of the times 'signature' will raise ValueError.
+        # But, it can also raise AttributeError, and, maybe something
+        # else. So to be fully backwards compatible, we catch all
+        # possible exceptions here, and reraise a TypeError.
+        raise TypeError('unsupported callable') from ex
+
+    args = []
+    varargs = None
+    varkw = None
+    kwonlyargs = []
+    defaults = ()
+    annotations = {}
+    defaults = ()
+    kwdefaults = {}
+
+    if sig.return_annotation is not sig.empty:
+        annotations['return'] = sig.return_annotation
+
+    for param in sig.parameters.values():
+        kind = param.kind
+        name = param.name
+
+        if kind is _POSITIONAL_ONLY:
+            args.append(name)
+        elif kind is _POSITIONAL_OR_KEYWORD:
+            args.append(name)
+            if param.default is not param.empty:
+                defaults += (param.default,)
+        elif kind is _VAR_POSITIONAL:
+            varargs = name
+        elif kind is _KEYWORD_ONLY:
+            kwonlyargs.append(name)
+            if param.default is not param.empty:
+                kwdefaults[name] = param.default
+        elif kind is _VAR_KEYWORD:
+            varkw = name
+
+        if param.annotation is not param.empty:
+            annotations[name] = param.annotation
+
+    if not kwdefaults:
+        # compatibility with 'func.__kwdefaults__'
+        kwdefaults = None
+
+    if not defaults:
+        # compatibility with 'func.__defaults__'
+        defaults = None
+
+    if builtin_method_param and (not args or args[0] != builtin_method_param):
+        # `func` is a method, and we always need to return its
+        # first parameter -- usually "self" (to be backwards
+        # compatible with the previous implementation of
+        # getfullargspec)
+        args.insert(0, builtin_method_param)
+
+    return FullArgSpec(args, varargs, varkw, defaults,
+                       kwonlyargs, kwdefaults, annotations)
+
 
 ArgInfo = namedtuple('ArgInfo', 'args varargs keywords locals')
 
@@ -1524,6 +1601,28 @@ def _signature_is_builtin(obj):
             obj in (type, object))
 
 
+def _signature_get_bound_param(spec):
+    # Internal helper to get first parameter name from a
+    # __text_signature__ of a builtin method, which should
+    # be in the following format: '($param1, ...)'.
+    # Assumptions are that the first argument won't have
+    # a default value or an annotation.
+
+    assert spec.startswith('($')
+
+    pos = spec.find(',')
+    if pos == -1:
+        pos = spec.find(')')
+
+    cpos = spec.find(':')
+    assert cpos == -1 or cpos > pos
+
+    cpos = spec.find('=')
+    assert cpos == -1 or cpos > pos
+
+    return spec[2:pos]
+
+
 def signature(obj):
     '''Get a signature object for the passed callable.'''
 
index 6ee2c3060d76b804e97eb1504ca3055fe38b051f..546fec5ff03e8d7b0998f91b367df6f3896578a0 100644 (file)
@@ -578,6 +578,36 @@ class TestClassesAndFunctions(unittest.TestCase):
                                      kwonlyargs_e=['arg'],
                                      formatted='(*, arg)')
 
+    def test_getfullargspec_signature_attr(self):
+        def test():
+            pass
+        spam_param = inspect.Parameter('spam', inspect.Parameter.POSITIONAL_ONLY)
+        test.__signature__ = inspect.Signature(parameters=(spam_param,))
+
+        self.assertFullArgSpecEquals(test, args_e=['spam'], formatted='(spam)')
+
+    @unittest.skipIf(MISSING_C_DOCSTRINGS,
+                     "Signature information for builtins requires docstrings")
+    def test_getfullargspec_builtin_methods(self):
+        self.assertFullArgSpecEquals(_pickle.Pickler.dump,
+                                     args_e=['self', 'obj'], formatted='(self, obj)')
+
+        self.assertFullArgSpecEquals(_pickle.Pickler(io.BytesIO()).dump,
+                                     args_e=['self', 'obj'], formatted='(self, obj)')
+
+    @unittest.skipIf(MISSING_C_DOCSTRINGS,
+                     "Signature information for builtins requires docstrings")
+    def test_getfullagrspec_builtin_func(self):
+        builtin = _testcapi.docstring_with_signature_with_defaults
+        spec = inspect.getfullargspec(builtin)
+        self.assertEqual(spec.defaults[0], 'avocado')
+
+    @unittest.skipIf(MISSING_C_DOCSTRINGS,
+                     "Signature information for builtins requires docstrings")
+    def test_getfullagrspec_builtin_func_no_signature(self):
+        builtin = _testcapi.docstring_no_signature
+        with self.assertRaises(TypeError):
+            inspect.getfullargspec(builtin)
 
     def test_getargspec_method(self):
         class A(object):
@@ -2614,6 +2644,15 @@ class TestBoundArguments(unittest.TestCase):
         self.assertNotEqual(ba, ba4)
 
 
+class TestSignaturePrivateHelpers(unittest.TestCase):
+    def test_signature_get_bound_param(self):
+        getter = inspect._signature_get_bound_param
+
+        self.assertEqual(getter('($self)'), 'self')
+        self.assertEqual(getter('($self, obj)'), 'self')
+        self.assertEqual(getter('($cls, /, obj)'), 'cls')
+
+
 class TestUnwrap(unittest.TestCase):
 
     def test_unwrap_one(self):
@@ -2719,7 +2758,8 @@ def test_main():
         TestGetcallargsFunctions, TestGetcallargsMethods,
         TestGetcallargsUnboundMethods, TestGetattrStatic, TestGetGeneratorState,
         TestNoEOL, TestSignatureObject, TestSignatureBind, TestParameterObject,
-        TestBoundArguments, TestGetClosureVars, TestUnwrap, TestMain
+        TestBoundArguments, TestSignaturePrivateHelpers, TestGetClosureVars,
+        TestUnwrap, TestMain
     )
 
 if __name__ == "__main__":
index 4dfc347e240c313ec37e325831e8832da394e46f..0807f0c11091731c4adf758b40274281cff64a7b 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -47,6 +47,8 @@ Library
 - Issue #20105: the codec exception chaining now correctly sets the
   traceback of the original exception as its __traceback__ attribute.
 
+- Issue #17481: inspect.getfullargspec() now uses inspect.signature() API.
+
 IDLE
 ----