From 646f6c3096abfe5bde13f039ebf32bce5baf083a Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Thu, 10 Aug 2017 00:46:29 -0400 Subject: [PATCH] [3.6] bpo-19903: IDLE: Calltips changed to use inspect.signature (GH-2822) (#3053) 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.. (cherry picked from commit 3b0f620c1a2a21272a9e2aeca6ca1d1ac10f8162) --- Lib/idlelib/calltips.py | 36 +++++++++------ Lib/idlelib/idle_test/test_calltips.py | 44 +++++++++++++++---- .../2017-08-03-14-08-42.bpo-19903.sqE1FS.rst | 3 ++ 3 files changed, 60 insertions(+), 23 deletions(-) create mode 100644 Misc/NEWS.d/next/IDLE/2017-08-03-14-08-42.bpo-19903.sqE1FS.rst diff --git a/Lib/idlelib/calltips.py b/Lib/idlelib/calltips.py index a8a3abe6c6..49625eac15 100644 --- a/Lib/idlelib/calltips.py +++ b/Lib/idlelib/calltips.py @@ -123,6 +123,8 @@ _MAX_LINES = 5 # enough for bytes _INDENT = ' '*4 # for wrapped signatures _first_param = re.compile(r'(?<=\()\w*\,?\s*') _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): @@ -134,25 +136,30 @@ def get_argspec(ob): empty line or _MAX_LINES. For builtins, this typically includes the arguments in addition to the return value. ''' - argspec = "" + argspec = default = "" try: ob_call = ob.__call__ except BaseException: - return argspec - if isinstance(ob, type): - fob = ob.__init__ - elif isinstance(ob_call, types.MethodType): - fob = ob_call - else: - fob = ob - if isinstance(fob, (types.FunctionType, types.MethodType)): - argspec = inspect.formatargspec(*inspect.getfullargspec(fob)) - if (isinstance(ob, (type, types.MethodType)) or - isinstance(ob_call, types.MethodType)): - argspec = _first_param.sub("", argspec) + return default + + fob = ob_call if isinstance(ob_call, types.MethodType) else ob + + try: + argspec = str(inspect.signature(fob)) + except ValueError as err: + msg = str(err) + if msg.startswith(_invalid_method): + return _invalid_method + + 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) - 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): doc = ob_call.__doc__ @@ -171,6 +178,7 @@ def get_argspec(ob): argspec = _default_callable_argspec return argspec + if __name__ == '__main__': from unittest import main main('idlelib.idle_test.test_calltips', verbosity=2) diff --git a/Lib/idlelib/idle_test/test_calltips.py b/Lib/idlelib/idle_test/test_calltips.py index 97e15fe10e..f6b71306ac 100644 --- a/Lib/idlelib/idle_test/test_calltips.py +++ b/Lib/idlelib/idle_test/test_calltips.py @@ -46,6 +46,7 @@ class Get_signatureTest(unittest.TestCase): # Python class that inherits builtin methods class List(list): "List() doc" + # Simulate builtin with no docstring for default tip test class SB: __call__ = None @@ -53,18 +54,29 @@ class Get_signatureTest(unittest.TestCase): self.assertEqual(signature(obj), out) if List.__doc__ is not None: - gtest(List, List.__doc__) + gtest(List, List.__doc__) # This and append_doc changed in 3.7. 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__, + '(self, /, *args, **kwargs)' + ct._argument_positional + '\n' + 'Initialize self. See help(type(self)) for accurate signature.') - append_doc = "L.append(object) -> None -- append object to end" #see3.7 + + append_doc = "L.append(object) -> None -- append object to end" gtest(list.append, append_doc) gtest([].append, append_doc) gtest(List.append, append_doc) gtest(types.MethodType, "method(function, instance)") 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): if textwrap.TextWrapper.__doc__ is not None: @@ -132,12 +144,20 @@ bytes() -> empty bytes object''') # test that starred first parameter is *not* removed from argspec class C: def m1(*args): pass - def m2(**kwds): pass c = C() - for meth, mtip in ((C.m1, '(*args)'), (c.m1, "(*args)"), - (C.m2, "(**kwds)"), (c.m2, "(**kwds)"),): + for meth, mtip in ((C.m1, '(*args)'), (c.m1, "(*args)"),): 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): # test that re works to delete a first parameter name that # includes non-ascii chars, such as various forms of A. @@ -156,17 +176,23 @@ bytes() -> empty bytes object''') class NoCall: def __getattr__(self, name): raise BaseException - class Call(NoCall): + class CallA(NoCall): + def __call__(oui, a, b, c): + pass + class CallB(NoCall): def __call__(self, ci): 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) def test_non_callables(self): for obj in (0, 0.0, '0', b'0', [], {}): self.assertEqual(signature(obj), '') + class Get_entityTest(unittest.TestCase): def test_bad_entity(self): self.assertIsNone(ct.get_entity('1/0')) diff --git a/Misc/NEWS.d/next/IDLE/2017-08-03-14-08-42.bpo-19903.sqE1FS.rst b/Misc/NEWS.d/next/IDLE/2017-08-03-14-08-42.bpo-19903.sqE1FS.rst new file mode 100644 index 0000000000..f25fc80c3d --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2017-08-03-14-08-42.bpo-19903.sqE1FS.rst @@ -0,0 +1,3 @@ +IDLE: Calltips use `inspect.signature` instead of `inspect.getfullargspec`. +This improves calltips for builtins converted to use Argument Clinic. +Patch by Louie Lu. -- 2.40.0