Commit 3b0f620c authored by Louie Lu's avatar Louie Lu Committed by Terry Jan Reedy

bpo-19903: IDLE: Calltips changed to use inspect.signature (#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.
parent 3ca9f50f
...@@ -123,6 +123,8 @@ _MAX_LINES = 5 # enough for bytes ...@@ -123,6 +123,8 @@ _MAX_LINES = 5 # enough for bytes
_INDENT = ' '*4 # for wrapped signatures _INDENT = ' '*4 # for wrapped signatures
_first_param = re.compile(r'(?<=\()\w*\,?\s*') _first_param = re.compile(r'(?<=\()\w*\,?\s*')
_default_callable_argspec = "See source or doc" _default_callable_argspec = "See source or doc"
_invalid_method = "invalid method signature"
_argument_positional = "\n['/' marks preceding arguments as positional-only]\n"
def get_argspec(ob): def get_argspec(ob):
...@@ -134,25 +136,30 @@ def get_argspec(ob): ...@@ -134,25 +136,30 @@ def get_argspec(ob):
empty line or _MAX_LINES. For builtins, this typically includes empty line or _MAX_LINES. For builtins, this typically includes
the arguments in addition to the return value. the arguments in addition to the return value.
''' '''
argspec = "" argspec = default = ""
try: try:
ob_call = ob.__call__ ob_call = ob.__call__
except BaseException: except BaseException:
return argspec return default
if isinstance(ob, type):
fob = ob.__init__ fob = ob_call if isinstance(ob_call, types.MethodType) else ob
elif isinstance(ob_call, types.MethodType):
fob = ob_call try:
else: argspec = str(inspect.signature(fob))
fob = ob except ValueError as err:
if isinstance(fob, (types.FunctionType, types.MethodType)): msg = str(err)
argspec = inspect.formatargspec(*inspect.getfullargspec(fob)) if msg.startswith(_invalid_method):
if (isinstance(ob, (type, types.MethodType)) or return _invalid_method
isinstance(ob_call, types.MethodType)):
argspec = _first_param.sub("", argspec) if '/' in argspec:
"""Using AC's positional argument should add the explain"""
argspec += _argument_positional
if isinstance(fob, type) and argspec == '()':
"""fob with no argument, use default callable argspec"""
argspec = _default_callable_argspec
lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT) lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT)
if len(argspec) > _MAX_COLS else [argspec] if argspec else []) if len(argspec) > _MAX_COLS else [argspec] if argspec else [])
if isinstance(ob_call, types.MethodType): if isinstance(ob_call, types.MethodType):
doc = ob_call.__doc__ doc = ob_call.__doc__
...@@ -171,6 +178,7 @@ def get_argspec(ob): ...@@ -171,6 +178,7 @@ def get_argspec(ob):
argspec = _default_callable_argspec argspec = _default_callable_argspec
return argspec return argspec
if __name__ == '__main__': if __name__ == '__main__':
from unittest import main from unittest import main
main('idlelib.idle_test.test_calltips', verbosity=2) main('idlelib.idle_test.test_calltips', verbosity=2)
...@@ -46,6 +46,7 @@ class Get_signatureTest(unittest.TestCase): ...@@ -46,6 +46,7 @@ class Get_signatureTest(unittest.TestCase):
# Python class that inherits builtin methods # Python class that inherits builtin methods
class List(list): "List() doc" class List(list): "List() doc"
# Simulate builtin with no docstring for default tip test # Simulate builtin with no docstring for default tip test
class SB: __call__ = None class SB: __call__ = None
...@@ -53,18 +54,28 @@ class Get_signatureTest(unittest.TestCase): ...@@ -53,18 +54,28 @@ class Get_signatureTest(unittest.TestCase):
self.assertEqual(signature(obj), out) self.assertEqual(signature(obj), out)
if List.__doc__ is not None: if List.__doc__ is not None:
gtest(List, List.__doc__) gtest(List, '(iterable=(), /)' + ct._argument_positional + '\n' +
List.__doc__)
gtest(list.__new__, gtest(list.__new__,
'Create and return a new object. See help(type) for accurate signature.') '(*args, **kwargs)\nCreate and return a new object. See help(type) for accurate signature.')
gtest(list.__init__, gtest(list.__init__,
'(self, /, *args, **kwargs)' + ct._argument_positional + '\n' +
'Initialize self. See help(type(self)) for accurate signature.') 'Initialize self. See help(type(self)) for accurate signature.')
append_doc = "Append object to the end of the list." append_doc = ct._argument_positional + "\nAppend object to the end of the list."
gtest(list.append, append_doc) gtest(list.append, '(self, object, /)' + append_doc)
gtest([].append, append_doc) gtest(List.append, '(self, object, /)' + append_doc)
gtest(List.append, append_doc) gtest([].append, '(object, /)' + append_doc)
gtest(types.MethodType, "method(function, instance)") gtest(types.MethodType, "method(function, instance)")
gtest(SB(), default_tip) gtest(SB(), default_tip)
import re
p = re.compile('')
gtest(re.sub, '''(pattern, repl, string, count=0, flags=0)\nReturn the string obtained by replacing the leftmost
non-overlapping occurrences of the pattern in string by the
replacement repl. repl can be either a string or a callable;
if a string, backslash escapes in it are processed. If it is
a callable, it's passed the match object and must return''')
gtest(p.sub, '''(repl, string, count=0)\nReturn the string obtained by replacing the leftmost non-overlapping occurrences o...''')
def test_signature_wrap(self): def test_signature_wrap(self):
if textwrap.TextWrapper.__doc__ is not None: if textwrap.TextWrapper.__doc__ is not None:
...@@ -132,12 +143,20 @@ bytes() -> empty bytes object''') ...@@ -132,12 +143,20 @@ bytes() -> empty bytes object''')
# test that starred first parameter is *not* removed from argspec # test that starred first parameter is *not* removed from argspec
class C: class C:
def m1(*args): pass def m1(*args): pass
def m2(**kwds): pass
c = C() c = C()
for meth, mtip in ((C.m1, '(*args)'), (c.m1, "(*args)"), for meth, mtip in ((C.m1, '(*args)'), (c.m1, "(*args)"),):
(C.m2, "(**kwds)"), (c.m2, "(**kwds)"),):
self.assertEqual(signature(meth), mtip) self.assertEqual(signature(meth), mtip)
def test_invalid_method_signature(self):
class C:
def m2(**kwargs): pass
class Test:
def __call__(*, a): pass
mtip = ct._invalid_method
self.assertEqual(signature(C().m2), mtip)
self.assertEqual(signature(Test()), mtip)
def test_non_ascii_name(self): def test_non_ascii_name(self):
# test that re works to delete a first parameter name that # test that re works to delete a first parameter name that
# includes non-ascii chars, such as various forms of A. # includes non-ascii chars, such as various forms of A.
...@@ -156,17 +175,23 @@ bytes() -> empty bytes object''') ...@@ -156,17 +175,23 @@ bytes() -> empty bytes object''')
class NoCall: class NoCall:
def __getattr__(self, name): def __getattr__(self, name):
raise BaseException raise BaseException
class Call(NoCall): class CallA(NoCall):
def __call__(oui, a, b, c):
pass
class CallB(NoCall):
def __call__(self, ci): def __call__(self, ci):
pass pass
for meth, mtip in ((NoCall, default_tip), (Call, default_tip),
(NoCall(), ''), (Call(), '(ci)')): for meth, mtip in ((NoCall, default_tip), (CallA, default_tip),
(NoCall(), ''), (CallA(), '(a, b, c)'),
(CallB(), '(ci)')):
self.assertEqual(signature(meth), mtip) self.assertEqual(signature(meth), mtip)
def test_non_callables(self): def test_non_callables(self):
for obj in (0, 0.0, '0', b'0', [], {}): for obj in (0, 0.0, '0', b'0', [], {}):
self.assertEqual(signature(obj), '') self.assertEqual(signature(obj), '')
class Get_entityTest(unittest.TestCase): class Get_entityTest(unittest.TestCase):
def test_bad_entity(self): def test_bad_entity(self):
self.assertIsNone(ct.get_entity('1/0')) self.assertIsNone(ct.get_entity('1/0'))
......
IDLE: Calltips use `inspect.signature` instead of `inspect.getfullargspec`.
This improves calltips for builtins converted to use Argument Clinic.
Patch by Louie Lu.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment