Skip to content

Commit 57d240e

Browse files
committed
inspect: Fix getfullargspec() to not to follow __wrapped__ chains
Initial patch by Nick Coghlan.
1 parent 4ac30f1 commit 57d240e

File tree

3 files changed

+109
-46
lines changed

3 files changed

+109
-46
lines changed

Lib/inspect.py

+65-46
Original file line numberDiff line numberDiff line change
@@ -949,9 +949,9 @@ def getfullargspec(func):
949949
The first four items in the tuple correspond to getargspec().
950950
"""
951951

952-
builtin_method_param = None
953-
954-
if ismethod(func):
952+
try:
953+
# Re: `skip_bound_arg=False`
954+
#
955955
# There is a notable difference in behaviour between getfullargspec
956956
# and Signature: the former always returns 'self' parameter for bound
957957
# methods, whereas the Signature always shows the actual calling
@@ -960,20 +960,15 @@ def getfullargspec(func):
960960
# To simulate this behaviour, we "unbind" bound methods, to trick
961961
# inspect.signature to always return their first parameter ("self",
962962
# usually)
963-
func = func.__func__
964963

965-
elif isbuiltin(func):
966-
# We have a builtin function or method. For that, we check the
967-
# special '__text_signature__' attribute, provided by the
968-
# Argument Clinic. If it's a method, we'll need to make sure
969-
# that its first parameter (usually "self") is always returned
970-
# (see the previous comment).
971-
text_signature = getattr(func, '__text_signature__', None)
972-
if text_signature and text_signature.startswith('($'):
973-
builtin_method_param = _signature_get_bound_param(text_signature)
964+
# Re: `follow_wrapper_chains=False`
965+
#
966+
# getfullargspec() historically ignored __wrapped__ attributes,
967+
# so we ensure that remains the case in 3.3+
974968

975-
try:
976-
sig = signature(func)
969+
sig = _signature_internal(func,
970+
follow_wrapper_chains=False,
971+
skip_bound_arg=False)
977972
except Exception as ex:
978973
# Most of the times 'signature' will raise ValueError.
979974
# But, it can also raise AttributeError, and, maybe something
@@ -1023,13 +1018,6 @@ def getfullargspec(func):
10231018
# compatibility with 'func.__defaults__'
10241019
defaults = None
10251020

1026-
if builtin_method_param and (not args or args[0] != builtin_method_param):
1027-
# `func` is a method, and we always need to return its
1028-
# first parameter -- usually "self" (to be backwards
1029-
# compatible with the previous implementation of
1030-
# getfullargspec)
1031-
args.insert(0, builtin_method_param)
1032-
10331021
return FullArgSpec(args, varargs, varkw, defaults,
10341022
kwonlyargs, kwdefaults, annotations)
10351023

@@ -1719,7 +1707,7 @@ def _signature_strip_non_python_syntax(signature):
17191707
return clean_signature, self_parameter, last_positional_only
17201708

17211709

1722-
def _signature_fromstr(cls, obj, s):
1710+
def _signature_fromstr(cls, obj, s, skip_bound_arg=True):
17231711
# Internal helper to parse content of '__text_signature__'
17241712
# and return a Signature based on it
17251713
Parameter = cls._parameter_cls
@@ -1840,7 +1828,7 @@ def p(name_node, default_node, default=empty):
18401828

18411829
if self_parameter is not None:
18421830
assert parameters
1843-
if getattr(obj, '__self__', None):
1831+
if getattr(obj, '__self__', None) and skip_bound_arg:
18441832
# strip off self, it's already been bound
18451833
parameters.pop(0)
18461834
else:
@@ -1851,20 +1839,39 @@ def p(name_node, default_node, default=empty):
18511839
return cls(parameters, return_annotation=cls.empty)
18521840

18531841

1854-
def signature(obj):
1855-
'''Get a signature object for the passed callable.'''
1842+
def _signature_from_builtin(cls, func, skip_bound_arg=True):
1843+
# Internal helper function to get signature for
1844+
# builtin callables
1845+
if not _signature_is_builtin(func):
1846+
raise TypeError("{!r} is not a Python builtin "
1847+
"function".format(func))
1848+
1849+
s = getattr(func, "__text_signature__", None)
1850+
if not s:
1851+
raise ValueError("no signature found for builtin {!r}".format(func))
1852+
1853+
return _signature_fromstr(cls, func, s, skip_bound_arg)
1854+
1855+
1856+
def _signature_internal(obj, follow_wrapper_chains=True, skip_bound_arg=True):
18561857

18571858
if not callable(obj):
18581859
raise TypeError('{!r} is not a callable object'.format(obj))
18591860

18601861
if isinstance(obj, types.MethodType):
18611862
# In this case we skip the first parameter of the underlying
18621863
# function (usually `self` or `cls`).
1863-
sig = signature(obj.__func__)
1864-
return _signature_bound_method(sig)
1864+
sig = _signature_internal(obj.__func__,
1865+
follow_wrapper_chains,
1866+
skip_bound_arg)
1867+
if skip_bound_arg:
1868+
return _signature_bound_method(sig)
1869+
else:
1870+
return sig
18651871

18661872
# Was this function wrapped by a decorator?
1867-
obj = unwrap(obj, stop=(lambda f: hasattr(f, "__signature__")))
1873+
if follow_wrapper_chains:
1874+
obj = unwrap(obj, stop=(lambda f: hasattr(f, "__signature__")))
18681875

18691876
try:
18701877
sig = obj.__signature__
@@ -1887,7 +1894,9 @@ def signature(obj):
18871894
# (usually `self`, or `cls`) will not be passed
18881895
# automatically (as for boundmethods)
18891896

1890-
wrapped_sig = signature(partialmethod.func)
1897+
wrapped_sig = _signature_internal(partialmethod.func,
1898+
follow_wrapper_chains,
1899+
skip_bound_arg)
18911900
sig = _signature_get_partial(wrapped_sig, partialmethod, (None,))
18921901

18931902
first_wrapped_param = tuple(wrapped_sig.parameters.values())[0]
@@ -1896,15 +1905,18 @@ def signature(obj):
18961905
return sig.replace(parameters=new_params)
18971906

18981907
if _signature_is_builtin(obj):
1899-
return Signature.from_builtin(obj)
1908+
return _signature_from_builtin(Signature, obj,
1909+
skip_bound_arg=skip_bound_arg)
19001910

19011911
if isfunction(obj) or _signature_is_functionlike(obj):
19021912
# If it's a pure Python function, or an object that is duck type
19031913
# of a Python function (Cython functions, for instance), then:
19041914
return Signature.from_function(obj)
19051915

19061916
if isinstance(obj, functools.partial):
1907-
wrapped_sig = signature(obj.func)
1917+
wrapped_sig = _signature_internal(obj.func,
1918+
follow_wrapper_chains,
1919+
skip_bound_arg)
19081920
return _signature_get_partial(wrapped_sig, obj)
19091921

19101922
sig = None
@@ -1915,17 +1927,23 @@ def signature(obj):
19151927
# in its metaclass
19161928
call = _signature_get_user_defined_method(type(obj), '__call__')
19171929
if call is not None:
1918-
sig = signature(call)
1930+
sig = _signature_internal(call,
1931+
follow_wrapper_chains,
1932+
skip_bound_arg)
19191933
else:
19201934
# Now we check if the 'obj' class has a '__new__' method
19211935
new = _signature_get_user_defined_method(obj, '__new__')
19221936
if new is not None:
1923-
sig = signature(new)
1937+
sig = _signature_internal(new,
1938+
follow_wrapper_chains,
1939+
skip_bound_arg)
19241940
else:
19251941
# Finally, we should have at least __init__ implemented
19261942
init = _signature_get_user_defined_method(obj, '__init__')
19271943
if init is not None:
1928-
sig = signature(init)
1944+
sig = _signature_internal(init,
1945+
follow_wrapper_chains,
1946+
skip_bound_arg)
19291947

19301948
if sig is None:
19311949
# At this point we know, that `obj` is a class, with no user-
@@ -1967,15 +1985,20 @@ def signature(obj):
19671985
call = _signature_get_user_defined_method(type(obj), '__call__')
19681986
if call is not None:
19691987
try:
1970-
sig = signature(call)
1988+
sig = _signature_internal(call,
1989+
follow_wrapper_chains,
1990+
skip_bound_arg)
19711991
except ValueError as ex:
19721992
msg = 'no signature found for {!r}'.format(obj)
19731993
raise ValueError(msg) from ex
19741994

19751995
if sig is not None:
19761996
# For classes and objects we skip the first parameter of their
19771997
# __call__, __new__, or __init__ methods
1978-
return _signature_bound_method(sig)
1998+
if skip_bound_arg:
1999+
return _signature_bound_method(sig)
2000+
else:
2001+
return sig
19792002

19802003
if isinstance(obj, types.BuiltinFunctionType):
19812004
# Raise a nicer error message for builtins
@@ -1984,6 +2007,10 @@ def signature(obj):
19842007

19852008
raise ValueError('callable {!r} is not supported by signature'.format(obj))
19862009

2010+
def signature(obj):
2011+
'''Get a signature object for the passed callable.'''
2012+
return _signature_internal(obj)
2013+
19872014

19882015
class _void:
19892016
'''A private marker - used in Parameter & Signature'''
@@ -2417,15 +2444,7 @@ def from_function(cls, func):
24172444

24182445
@classmethod
24192446
def from_builtin(cls, func):
2420-
if not _signature_is_builtin(func):
2421-
raise TypeError("{!r} is not a Python builtin "
2422-
"function".format(func))
2423-
2424-
s = getattr(func, "__text_signature__", None)
2425-
if not s:
2426-
raise ValueError("no signature found for builtin {!r}".format(func))
2427-
2428-
return _signature_fromstr(cls, func, s)
2447+
return _signature_from_builtin(cls, func)
24292448

24302449
@property
24312450
def parameters(self):

Lib/test/test_inspect.py

+40
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,46 @@ def test_getfullargspec(self):
577577
kwonlyargs_e=['arg'],
578578
formatted='(*, arg)')
579579

580+
def test_argspec_api_ignores_wrapped(self):
581+
# Issue 20684: low level introspection API must ignore __wrapped__
582+
@functools.wraps(mod.spam)
583+
def ham(x, y):
584+
pass
585+
# Basic check
586+
self.assertArgSpecEquals(ham, ['x', 'y'], formatted='(x, y)')
587+
self.assertFullArgSpecEquals(ham, ['x', 'y'], formatted='(x, y)')
588+
self.assertFullArgSpecEquals(functools.partial(ham),
589+
['x', 'y'], formatted='(x, y)')
590+
# Other variants
591+
def check_method(f):
592+
self.assertArgSpecEquals(f, ['self', 'x', 'y'],
593+
formatted='(self, x, y)')
594+
class C:
595+
@functools.wraps(mod.spam)
596+
def ham(self, x, y):
597+
pass
598+
pham = functools.partialmethod(ham)
599+
@functools.wraps(mod.spam)
600+
def __call__(self, x, y):
601+
pass
602+
check_method(C())
603+
check_method(C.ham)
604+
check_method(C().ham)
605+
check_method(C.pham)
606+
check_method(C().pham)
607+
608+
class C_new:
609+
@functools.wraps(mod.spam)
610+
def __new__(self, x, y):
611+
pass
612+
check_method(C_new)
613+
614+
class C_init:
615+
@functools.wraps(mod.spam)
616+
def __init__(self, x, y):
617+
pass
618+
check_method(C_init)
619+
580620
def test_getfullargspec_signature_attr(self):
581621
def test():
582622
pass

Misc/NEWS

+4
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ Library
6666
loop.set_exception_handler(), loop.default_exception_handler(), and
6767
loop.call_exception_handler().
6868

69+
- Issue #20684: Fix inspect.getfullargspec() to not to follow __wrapped__
70+
chains. Make its behaviour consistent with bound methods first argument.
71+
Patch by Nick Coghlan and Yury Selivanov.
72+
6973
Tests
7074
-----
7175

0 commit comments

Comments
 (0)