From 8c8d6aa27e0c7940d49f1eeef65879f5231aa70b Mon Sep 17 00:00:00 2001 From: Louie Lu Date: Tue, 2 May 2017 18:24:55 +0800 Subject: [PATCH 01/10] bpo-19903: Change to inspect.signature for calltips This commit change the get_argspec from using inspect.getfullargspec to inspect.signature. It will improve the tip message for use. Also, if object is not callable, now will return this message for user, not blank tips. If the methods has an invalid method signature, it will also return message to user. --- Lib/idlelib/calltips.py | 13 +++++++++++-- Lib/idlelib/idle_test/test_calltips.py | 16 ++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/Lib/idlelib/calltips.py b/Lib/idlelib/calltips.py index 4c5aea2acbf40b..4c5dbb61a45871 100644 --- a/Lib/idlelib/calltips.py +++ b/Lib/idlelib/calltips.py @@ -136,6 +136,9 @@ def get_argspec(ob): argspec = "" try: ob_call = ob.__call__ + except AttributeError: + argspec = "Object is not callable" + return argspec except BaseException: return argspec if isinstance(ob, type): @@ -145,10 +148,16 @@ def get_argspec(ob): else: fob = ob if isinstance(fob, (types.FunctionType, types.MethodType)): - argspec = inspect.formatargspec(*inspect.getfullargspec(fob)) + try: + argspec = str(inspect.signature(fob)) + except ValueError: + argspec = "This function has an invalid method signature" + return argspec + if (isinstance(ob, (type, types.MethodType)) or isinstance(ob_call, types.MethodType)): - argspec = _first_param.sub("", argspec) + if argspec.startswith("(self,"): + argspec = _first_param.sub("", argspec) lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT) if len(argspec) > _MAX_COLS else [argspec] if argspec else []) diff --git a/Lib/idlelib/idle_test/test_calltips.py b/Lib/idlelib/idle_test/test_calltips.py index 29b9f06faf868b..9ab9bc9f20493a 100644 --- a/Lib/idlelib/idle_test/test_calltips.py +++ b/Lib/idlelib/idle_test/test_calltips.py @@ -132,12 +132,20 @@ def test_starred_parameter(self): # 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 = "This function has an invalid method signature" + 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. @@ -165,7 +173,7 @@ def __call__(self, ci): def test_non_callables(self): for obj in (0, 0.0, '0', b'0', [], {}): - self.assertEqual(signature(obj), '') + self.assertEqual(signature(obj), 'Object is not callable') class Get_entityTest(unittest.TestCase): def test_bad_entity(self): From c5ef4d1186caba7c594ec3059c84ad20aa8b1c18 Mon Sep 17 00:00:00 2001 From: Louie Lu Date: Tue, 2 May 2017 21:45:45 +0800 Subject: [PATCH 02/10] Revert object is not callable --- Lib/idlelib/calltips.py | 3 --- Lib/idlelib/idle_test/test_calltips.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/Lib/idlelib/calltips.py b/Lib/idlelib/calltips.py index 4c5dbb61a45871..e2a467a01db8fa 100644 --- a/Lib/idlelib/calltips.py +++ b/Lib/idlelib/calltips.py @@ -136,9 +136,6 @@ def get_argspec(ob): argspec = "" try: ob_call = ob.__call__ - except AttributeError: - argspec = "Object is not callable" - return argspec except BaseException: return argspec if isinstance(ob, type): diff --git a/Lib/idlelib/idle_test/test_calltips.py b/Lib/idlelib/idle_test/test_calltips.py index 9ab9bc9f20493a..cb5033c2c0cb18 100644 --- a/Lib/idlelib/idle_test/test_calltips.py +++ b/Lib/idlelib/idle_test/test_calltips.py @@ -171,9 +171,6 @@ def __call__(self, ci): (NoCall(), ''), (Call(), '(ci)')): self.assertEqual(signature(meth), mtip) - def test_non_callables(self): - for obj in (0, 0.0, '0', b'0', [], {}): - self.assertEqual(signature(obj), 'Object is not callable') class Get_entityTest(unittest.TestCase): def test_bad_entity(self): From f1cf2c2256821efebebd16fa2cd41d80ecc80c6c Mon Sep 17 00:00:00 2001 From: Louie Lu Date: Wed, 3 May 2017 12:06:48 +0800 Subject: [PATCH 03/10] Fix first param trim condition --- Lib/idlelib/calltips.py | 6 ++---- Lib/idlelib/idle_test/test_calltips.py | 10 +++++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Lib/idlelib/calltips.py b/Lib/idlelib/calltips.py index e2a467a01db8fa..8105e51efd90aa 100644 --- a/Lib/idlelib/calltips.py +++ b/Lib/idlelib/calltips.py @@ -151,10 +151,8 @@ def get_argspec(ob): argspec = "This function has an invalid method signature" return argspec - if (isinstance(ob, (type, types.MethodType)) or - isinstance(ob_call, types.MethodType)): - if argspec.startswith("(self,"): - argspec = _first_param.sub("", argspec) + if isinstance(ob, type): + argspec = _first_param.sub("", argspec) lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT) if len(argspec) > _MAX_COLS else [argspec] if argspec else []) diff --git a/Lib/idlelib/idle_test/test_calltips.py b/Lib/idlelib/idle_test/test_calltips.py index cb5033c2c0cb18..768a62bdb903e1 100644 --- a/Lib/idlelib/idle_test/test_calltips.py +++ b/Lib/idlelib/idle_test/test_calltips.py @@ -164,11 +164,15 @@ def test_attribute_exception(self): 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) From 837f88ae2087e311603d0f4058988ceed7e3a157 Mon Sep 17 00:00:00 2001 From: Louie Lu Date: Wed, 3 May 2017 12:10:25 +0800 Subject: [PATCH 04/10] Revert test_non_callables --- Lib/idlelib/idle_test/test_calltips.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/idlelib/idle_test/test_calltips.py b/Lib/idlelib/idle_test/test_calltips.py index 768a62bdb903e1..8017366b5881d8 100644 --- a/Lib/idlelib/idle_test/test_calltips.py +++ b/Lib/idlelib/idle_test/test_calltips.py @@ -175,6 +175,10 @@ def __call__(self, ci): (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): From a3bfdbd4d5bcced3845dac8b52603053412112a4 Mon Sep 17 00:00:00 2001 From: Louie Lu Date: Thu, 4 May 2017 17:08:30 +0800 Subject: [PATCH 05/10] Add built-in function test --- Lib/idlelib/calltips.py | 1 + Lib/idlelib/idle_test/test_calltips.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/idlelib/calltips.py b/Lib/idlelib/calltips.py index 8105e51efd90aa..d1ce6e4f44b814 100644 --- a/Lib/idlelib/calltips.py +++ b/Lib/idlelib/calltips.py @@ -122,6 +122,7 @@ def get_entity(expression): _INDENT = ' '*4 # for wrapped signatures _first_param = re.compile(r'(?<=\()\w*\,?\s*') _default_callable_argspec = "See source or doc" +_invalid_method = "invalid method signature" def get_argspec(ob): diff --git a/Lib/idlelib/idle_test/test_calltips.py b/Lib/idlelib/idle_test/test_calltips.py index 8017366b5881d8..7a150ff5e83a80 100644 --- a/Lib/idlelib/idle_test/test_calltips.py +++ b/Lib/idlelib/idle_test/test_calltips.py @@ -66,6 +66,11 @@ def gtest(obj, out): 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') + gtest(p.sub, '(repl, string, count=0)') + def test_signature_wrap(self): if textwrap.TextWrapper.__doc__ is not None: self.assertEqual(signature(textwrap.TextWrapper), '''\ @@ -142,7 +147,7 @@ def m2(**kwargs): pass class Test: def __call__(*, a): pass - mtip = "This function has an invalid method signature" + mtip = ct._invalid_method self.assertEqual(signature(C().m2), mtip) self.assertEqual(signature(Test()), mtip) From 13172f1c3d1feae6b055b516027d1f128b3c5e18 Mon Sep 17 00:00:00 2001 From: Louie Lu Date: Thu, 4 May 2017 17:10:56 +0800 Subject: [PATCH 06/10] Fix invalid method signature message --- Lib/idlelib/calltips.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/calltips.py b/Lib/idlelib/calltips.py index d1ce6e4f44b814..65e0fd0f363869 100644 --- a/Lib/idlelib/calltips.py +++ b/Lib/idlelib/calltips.py @@ -149,7 +149,7 @@ def get_argspec(ob): try: argspec = str(inspect.signature(fob)) except ValueError: - argspec = "This function has an invalid method signature" + argspec = _invalid_method return argspec if isinstance(ob, type): From b40056c3e96894671eb77b31a2101db06d96c1ba Mon Sep 17 00:00:00 2001 From: Louie Lu Date: Thu, 4 May 2017 17:16:25 +0800 Subject: [PATCH 07/10] Fix re.sub compare string --- Lib/idlelib/idle_test/test_calltips.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/idlelib/idle_test/test_calltips.py b/Lib/idlelib/idle_test/test_calltips.py index 7a150ff5e83a80..c3596d209a69ce 100644 --- a/Lib/idlelib/idle_test/test_calltips.py +++ b/Lib/idlelib/idle_test/test_calltips.py @@ -68,7 +68,12 @@ def gtest(obj, out): import re p = re.compile('') - gtest(re.sub, '(pattern, repl, string, count=0, flags=0') + gtest(re.sub, '''(pattern, repl, string, count=0, flags=0) +Return 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)') def test_signature_wrap(self): From a9f1825c19a782a111f79868847419bb182fbeb8 Mon Sep 17 00:00:00 2001 From: Louie Lu Date: Thu, 4 May 2017 23:02:40 +0800 Subject: [PATCH 08/10] Fix callable signature --- Lib/idlelib/calltips.py | 31 +++++++++++++++++--------- Lib/idlelib/idle_test/test_calltips.py | 11 +++++---- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/Lib/idlelib/calltips.py b/Lib/idlelib/calltips.py index 65e0fd0f363869..6ecc9c28e6e6f6 100644 --- a/Lib/idlelib/calltips.py +++ b/Lib/idlelib/calltips.py @@ -1,4 +1,4 @@ -"""calltips.py - An IDLE Extension to Jog Your Memory +b"""calltips.py - An IDLE Extension to Jog Your Memory Call Tips are floating windows which display function, class, and method parameter and docstring information when you type an opening parenthesis, and @@ -139,21 +139,32 @@ def get_argspec(ob): ob_call = ob.__call__ except BaseException: return argspec - if isinstance(ob, type): - fob = ob.__init__ - elif isinstance(ob_call, types.MethodType): + if isinstance(ob_call, types.MethodType): fob = ob_call else: fob = ob - if isinstance(fob, (types.FunctionType, types.MethodType)): - try: - argspec = str(inspect.signature(fob)) - except ValueError: + + try: + argspec = str(inspect.signature(fob)) + except ValueError as err: + msg = str(err) + if msg.startswith(_invalid_method): argspec = _invalid_method return argspec + elif msg.startswith('no signature found for'): + """If no signature found for function or method""" + pass + else: + """Callable is not supported by signature""" + pass - if isinstance(ob, type): - argspec = _first_param.sub("", argspec) + if '/' in argspec: + """Argument Clinic positional-only mark, ignore the result of signature + """ + argspec = '' + 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 []) diff --git a/Lib/idlelib/idle_test/test_calltips.py b/Lib/idlelib/idle_test/test_calltips.py index c3596d209a69ce..2e1742e3fda646 100644 --- a/Lib/idlelib/idle_test/test_calltips.py +++ b/Lib/idlelib/idle_test/test_calltips.py @@ -46,6 +46,7 @@ def test_builtins(self): # Python class that inherits builtin methods class List(list): "List() doc" + # Simulate builtin with no docstring for default tip test class SB: __call__ = None @@ -55,26 +56,23 @@ def gtest(obj, out): if List.__doc__ is not None: gtest(List, List.__doc__) 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__, 'Initialize self. See help(type(self)) for accurate signature.') append_doc = "Append object to the end of the list." 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) -Return the string obtained by replacing the leftmost + 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)') + 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: @@ -180,6 +178,7 @@ def __call__(oui, a, b, c): class CallB(NoCall): def __call__(self, ci): pass + for meth, mtip in ((NoCall, default_tip), (CallA, default_tip), (NoCall(), ''), (CallA(), '(a, b, c)'), (CallB(), '(ci)')): From 8caf548172055283b42a5535a3ef725353369c8d Mon Sep 17 00:00:00 2001 From: Louie Lu Date: Fri, 5 May 2017 11:57:08 +0800 Subject: [PATCH 09/10] Revert unexpect bytes string --- Lib/idlelib/calltips.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/calltips.py b/Lib/idlelib/calltips.py index 6ecc9c28e6e6f6..5aad42a400fc51 100644 --- a/Lib/idlelib/calltips.py +++ b/Lib/idlelib/calltips.py @@ -1,4 +1,4 @@ -b"""calltips.py - An IDLE Extension to Jog Your Memory +"""calltips.py - An IDLE Extension to Jog Your Memory Call Tips are floating windows which display function, class, and method parameter and docstring information when you type an opening parenthesis, and From a2b3829cd9d239a5cd2a5920c4859ea213c46eeb Mon Sep 17 00:00:00 2001 From: Louie Lu Date: Fri, 5 May 2017 15:31:42 +0800 Subject: [PATCH 10/10] Add AC argument positional explain and test --- Lib/idlelib/calltips.py | 6 +++--- Lib/idlelib/idle_test/test_calltips.py | 13 ++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Lib/idlelib/calltips.py b/Lib/idlelib/calltips.py index 5aad42a400fc51..0dcc970d451698 100644 --- a/Lib/idlelib/calltips.py +++ b/Lib/idlelib/calltips.py @@ -123,6 +123,7 @@ def get_entity(expression): _first_param = re.compile(r'(?<=\()\w*\,?\s*') _default_callable_argspec = "See source or doc" _invalid_method = "invalid method signature" +_argument_positional = "('/' marks preceding arguments as positional-only)" def get_argspec(ob): @@ -159,9 +160,8 @@ def get_argspec(ob): pass if '/' in argspec: - """Argument Clinic positional-only mark, ignore the result of signature - """ - argspec = '' + """Using AC's positional argument should add the explain""" + argspec = '\n'.join([argspec, _argument_positional]) if isinstance(fob, type) and argspec == '()': """fob with no argument, use default callable argspec""" argspec = _default_callable_argspec diff --git a/Lib/idlelib/idle_test/test_calltips.py b/Lib/idlelib/idle_test/test_calltips.py index 2e1742e3fda646..dbbdf14b45b207 100644 --- a/Lib/idlelib/idle_test/test_calltips.py +++ b/Lib/idlelib/idle_test/test_calltips.py @@ -54,15 +54,18 @@ def gtest(obj, out): self.assertEqual(signature(obj), out) if List.__doc__ is not None: - gtest(List, List.__doc__) + gtest(List, '(iterable=(), /)\n' + ct._argument_positional + '\n' + + List.__doc__) gtest(list.__new__, '(*args, **kwargs)\nCreate and return a new object. See help(type) for accurate signature.') gtest(list.__init__, + '(self, /, *args, **kwargs)\n' + ct._argument_positional + '\n' + 'Initialize self. See help(type(self)) for accurate signature.') - append_doc = "Append object to the end of the list." - gtest(list.append, append_doc) - gtest([].append, append_doc) - gtest(List.append, append_doc) + append_doc = ct._argument_positional + '\n' + "Append object to the end of the list." + gtest(list.append, '(self, object, /)\n' + append_doc) + gtest(List.append, '(self, object, /)\n' + append_doc) + gtest([].append, '(object, /)\n' + append_doc) + gtest(types.MethodType, "method(function, instance)") gtest(SB(), default_tip) import re