summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorYury Selivanov <yselivanov@sprymix.com>2014-01-29 16:24:39 (GMT)
committerYury Selivanov <yselivanov@sprymix.com>2014-01-29 16:24:39 (GMT)
commitd82eddcf058deb8140e2d8670a4f2109872a2015 (patch)
treed2a6b2a095874e3ee7ad3f72acbbb0a055974870
parent07a9e452accb432bd9da20b20e13f4bf8feebacb (diff)
downloadcpython-d82eddcf058deb8140e2d8670a4f2109872a2015.zip
cpython-d82eddcf058deb8140e2d8670a4f2109872a2015.tar.gz
cpython-d82eddcf058deb8140e2d8670a4f2109872a2015.tar.bz2
inspect.getfullargspec: Use inspect.signature API behind the scenes #17481
-rw-r--r--Doc/whatsnew/3.4.rst7
-rw-r--r--Lib/inspect.py111
-rw-r--r--Lib/test/test_inspect.py42
-rw-r--r--Misc/NEWS2
4 files changed, 155 insertions, 7 deletions
diff --git a/Doc/whatsnew/3.4.rst b/Doc/whatsnew/3.4.rst
index 4718339..d7a04f6 100644
--- a/Doc/whatsnew/3.4.rst
+++ b/Doc/whatsnew/3.4.rst
@@ -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
-------
diff --git a/Lib/inspect.py b/Lib/inspect.py
index f06138c..4f9c1d1 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -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.'''
diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py
index 6ee2c30..546fec5 100644
--- a/Lib/test/test_inspect.py
+++ b/Lib/test/test_inspect.py
@@ -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__":
diff --git a/Misc/NEWS b/Misc/NEWS
index 4dfc347..0807f0c 100644
--- 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
----