On Py3, use inspect.signature for more accurate signature calculation

This improves handling of wrapped functions and bound methods.

It turns out that we no longer need to hack in support for
functools.partial; inspect.signature handles this automatically. Added
a test to make sure this didn't/doesn't regress.
This commit is contained in:
Nathaniel J. Smith 2017-02-23 17:28:08 -08:00
parent b83dfdebf2
commit b56d93158a
2 changed files with 112 additions and 34 deletions

View File

@ -28,41 +28,64 @@ memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE)
if PY3: if PY3:
from functools import partial # Copied from the definition of inspect.getfullargspec from Python master,
# and modified to remove the use of special flags that break decorated
# callables and bound methods in the name of backwards compatibility. Used
# under the terms of PSF license v2, which requires the above statement
# and the following:
#
# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
# 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software
# Foundation; All Rights Reserved
def getargspec(func): def getargspec(func):
"""Like inspect.getargspec but supports functools.partial as well.""" """Like inspect.getfullargspec but supports bound methods, and wrapped
if inspect.ismethod(func): methods."""
func = func.__func__ sig = inspect.signature(func)
if type(func) is partial:
orig_func = func.func args = []
argspec = getargspec(orig_func) varargs = None
args = list(argspec[0]) varkw = None
defaults = list(argspec[3] or ()) kwonlyargs = []
kwoargs = list(argspec[4]) defaults = ()
kwodefs = dict(argspec[5] or {}) annotations = {}
if func.args: defaults = ()
args = args[len(func.args):] kwdefaults = {}
for arg in func.keywords or ():
try: if sig.return_annotation is not sig.empty:
i = args.index(arg) - len(args) annotations['return'] = sig.return_annotation
del args[i]
try: for param in sig.parameters.values():
del defaults[i] kind = param.kind
except IndexError: name = param.name
pass
except ValueError: # must be a kwonly arg if kind is inspect.Parameter.POSITIONAL_ONLY:
i = kwoargs.index(arg) args.append(name)
del kwoargs[i] elif kind is inspect.Parameter.POSITIONAL_OR_KEYWORD:
del kwodefs[arg] args.append(name)
return inspect.FullArgSpec(args, argspec[1], argspec[2], if param.default is not param.empty:
tuple(defaults), kwoargs, defaults += (param.default,)
kwodefs, argspec[6]) elif kind is inspect.Parameter.VAR_POSITIONAL:
while hasattr(func, '__wrapped__'): varargs = name
func = func.__wrapped__ elif kind is inspect.Parameter.KEYWORD_ONLY:
if not inspect.isfunction(func): kwonlyargs.append(name)
raise TypeError('%r is not a Python function' % func) if param.default is not param.empty:
return inspect.getfullargspec(func) kwdefaults[name] = param.default
elif kind is inspect.Parameter.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
return inspect.FullArgSpec(args, varargs, varkw, defaults,
kwonlyargs, kwdefaults, annotations)
else: # 2.7 else: # 2.7
from functools import partial from functools import partial

View File

@ -10,8 +10,63 @@
""" """
from unittest import TestCase from unittest import TestCase
from six import PY3
import functools
from textwrap import dedent
from sphinx.util import inspect from sphinx.util import inspect
class TestGetArgSpec(TestCase):
def test_getargspec_partial(self):
def fun(a, b, c=1, d=2):
pass
p = functools.partial(fun, 10, c=11)
if PY3:
# Python 3's partial is rather cleverer than Python 2's, and we
# have to jump through some hoops to define an equivalent function
# in a way that won't confuse Python 2's parser:
ns = {}
exec(dedent("""
def f_expected(b, *, c=11, d=2):
pass
"""), ns)
f_expected = ns["f_expected"]
else:
def f_expected(b, d=2):
pass
expected = inspect.getargspec(f_expected)
assert expected == inspect.getargspec(p)
def test_getargspec_bound_methods(self):
def f_expected_unbound(self, arg1, **kwargs):
pass
expected_unbound = inspect.getargspec(f_expected_unbound)
def f_expected_bound(arg1, **kwargs):
pass
expected_bound = inspect.getargspec(f_expected_bound)
class Foo:
def method(self, arg1, **kwargs):
pass
bound_method = Foo().method
@functools.wraps(bound_method)
def wrapped_bound_method(*args, **kwargs):
pass
assert expected_unbound == inspect.getargspec(Foo.method)
if PY3:
# On py2, the inspect functions don't properly handle bound
# methods (they include a spurious 'self' argument)
assert expected_bound == inspect.getargspec(bound_method)
# On py2, the inspect functions can't properly handle wrapped
# functions (no __wrapped__ support)
assert expected_bound == inspect.getargspec(wrapped_bound_method)
class TestSafeGetAttr(TestCase): class TestSafeGetAttr(TestCase):
def test_safe_getattr_with_default(self): def test_safe_getattr_with_default(self):