Skip to content

Commit 3b0f620

Browse files
mlouieluterryjreedy
authored andcommitted
bpo-19903: IDLE: Calltips changed to use inspect.signature (python#2822)
Idlelib.calltips.get_argspec now uses inspect.signature instead of inspect.getfullargspec, like help() does. This improves the signature in the call tip in a few different cases, including builtins converted to provide a signature. A message is added if the object is not callable, has an invalid signature, or if it has positional-only parameters. Patch by Louie Lu.
1 parent 3ca9f50 commit 3b0f620

File tree

3 files changed

+62
-26
lines changed

3 files changed

+62
-26
lines changed

Lib/idlelib/calltips.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ def get_entity(expression):
123123
_INDENT = ' '*4 # for wrapped signatures
124124
_first_param = re.compile(r'(?<=\()\w*\,?\s*')
125125
_default_callable_argspec = "See source or doc"
126+
_invalid_method = "invalid method signature"
127+
_argument_positional = "\n['/' marks preceding arguments as positional-only]\n"
126128

127129

128130
def get_argspec(ob):
@@ -134,25 +136,30 @@ def get_argspec(ob):
134136
empty line or _MAX_LINES. For builtins, this typically includes
135137
the arguments in addition to the return value.
136138
'''
137-
argspec = ""
139+
argspec = default = ""
138140
try:
139141
ob_call = ob.__call__
140142
except BaseException:
141-
return argspec
142-
if isinstance(ob, type):
143-
fob = ob.__init__
144-
elif isinstance(ob_call, types.MethodType):
145-
fob = ob_call
146-
else:
147-
fob = ob
148-
if isinstance(fob, (types.FunctionType, types.MethodType)):
149-
argspec = inspect.formatargspec(*inspect.getfullargspec(fob))
150-
if (isinstance(ob, (type, types.MethodType)) or
151-
isinstance(ob_call, types.MethodType)):
152-
argspec = _first_param.sub("", argspec)
143+
return default
144+
145+
fob = ob_call if isinstance(ob_call, types.MethodType) else ob
146+
147+
try:
148+
argspec = str(inspect.signature(fob))
149+
except ValueError as err:
150+
msg = str(err)
151+
if msg.startswith(_invalid_method):
152+
return _invalid_method
153+
154+
if '/' in argspec:
155+
"""Using AC's positional argument should add the explain"""
156+
argspec += _argument_positional
157+
if isinstance(fob, type) and argspec == '()':
158+
"""fob with no argument, use default callable argspec"""
159+
argspec = _default_callable_argspec
153160

154161
lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT)
155-
if len(argspec) > _MAX_COLS else [argspec] if argspec else [])
162+
if len(argspec) > _MAX_COLS else [argspec] if argspec else [])
156163

157164
if isinstance(ob_call, types.MethodType):
158165
doc = ob_call.__doc__
@@ -171,6 +178,7 @@ def get_argspec(ob):
171178
argspec = _default_callable_argspec
172179
return argspec
173180

181+
174182
if __name__ == '__main__':
175183
from unittest import main
176184
main('idlelib.idle_test.test_calltips', verbosity=2)

Lib/idlelib/idle_test/test_calltips.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,25 +46,36 @@ def test_builtins(self):
4646

4747
# Python class that inherits builtin methods
4848
class List(list): "List() doc"
49+
4950
# Simulate builtin with no docstring for default tip test
5051
class SB: __call__ = None
5152

5253
def gtest(obj, out):
5354
self.assertEqual(signature(obj), out)
5455

5556
if List.__doc__ is not None:
56-
gtest(List, List.__doc__)
57+
gtest(List, '(iterable=(), /)' + ct._argument_positional + '\n' +
58+
List.__doc__)
5759
gtest(list.__new__,
58-
'Create and return a new object. See help(type) for accurate signature.')
60+
'(*args, **kwargs)\nCreate and return a new object. See help(type) for accurate signature.')
5961
gtest(list.__init__,
62+
'(self, /, *args, **kwargs)' + ct._argument_positional + '\n' +
6063
'Initialize self. See help(type(self)) for accurate signature.')
61-
append_doc = "Append object to the end of the list."
62-
gtest(list.append, append_doc)
63-
gtest([].append, append_doc)
64-
gtest(List.append, append_doc)
64+
append_doc = ct._argument_positional + "\nAppend object to the end of the list."
65+
gtest(list.append, '(self, object, /)' + append_doc)
66+
gtest(List.append, '(self, object, /)' + append_doc)
67+
gtest([].append, '(object, /)' + append_doc)
6568

6669
gtest(types.MethodType, "method(function, instance)")
6770
gtest(SB(), default_tip)
71+
import re
72+
p = re.compile('')
73+
gtest(re.sub, '''(pattern, repl, string, count=0, flags=0)\nReturn the string obtained by replacing the leftmost
74+
non-overlapping occurrences of the pattern in string by the
75+
replacement repl. repl can be either a string or a callable;
76+
if a string, backslash escapes in it are processed. If it is
77+
a callable, it's passed the match object and must return''')
78+
gtest(p.sub, '''(repl, string, count=0)\nReturn the string obtained by replacing the leftmost non-overlapping occurrences o...''')
6879

6980
def test_signature_wrap(self):
7081
if textwrap.TextWrapper.__doc__ is not None:
@@ -132,12 +143,20 @@ def test_starred_parameter(self):
132143
# test that starred first parameter is *not* removed from argspec
133144
class C:
134145
def m1(*args): pass
135-
def m2(**kwds): pass
136146
c = C()
137-
for meth, mtip in ((C.m1, '(*args)'), (c.m1, "(*args)"),
138-
(C.m2, "(**kwds)"), (c.m2, "(**kwds)"),):
147+
for meth, mtip in ((C.m1, '(*args)'), (c.m1, "(*args)"),):
139148
self.assertEqual(signature(meth), mtip)
140149

150+
def test_invalid_method_signature(self):
151+
class C:
152+
def m2(**kwargs): pass
153+
class Test:
154+
def __call__(*, a): pass
155+
156+
mtip = ct._invalid_method
157+
self.assertEqual(signature(C().m2), mtip)
158+
self.assertEqual(signature(Test()), mtip)
159+
141160
def test_non_ascii_name(self):
142161
# test that re works to delete a first parameter name that
143162
# includes non-ascii chars, such as various forms of A.
@@ -156,17 +175,23 @@ def test_attribute_exception(self):
156175
class NoCall:
157176
def __getattr__(self, name):
158177
raise BaseException
159-
class Call(NoCall):
178+
class CallA(NoCall):
179+
def __call__(oui, a, b, c):
180+
pass
181+
class CallB(NoCall):
160182
def __call__(self, ci):
161183
pass
162-
for meth, mtip in ((NoCall, default_tip), (Call, default_tip),
163-
(NoCall(), ''), (Call(), '(ci)')):
184+
185+
for meth, mtip in ((NoCall, default_tip), (CallA, default_tip),
186+
(NoCall(), ''), (CallA(), '(a, b, c)'),
187+
(CallB(), '(ci)')):
164188
self.assertEqual(signature(meth), mtip)
165189

166190
def test_non_callables(self):
167191
for obj in (0, 0.0, '0', b'0', [], {}):
168192
self.assertEqual(signature(obj), '')
169193

194+
170195
class Get_entityTest(unittest.TestCase):
171196
def test_bad_entity(self):
172197
self.assertIsNone(ct.get_entity('1/0'))
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
IDLE: Calltips use `inspect.signature` instead of `inspect.getfullargspec`.
2+
This improves calltips for builtins converted to use Argument Clinic.
3+
Patch by Louie Lu.

0 commit comments

Comments
 (0)