]> granicus.if.org Git - python/commitdiff
bpo-19903: IDLE: Calltips changed to use inspect.signature (#2822)
authorLouie Lu <git@louie.lu>
Thu, 10 Aug 2017 00:58:13 +0000 (08:58 +0800)
committerTerry Jan Reedy <tjreedy@udel.edu>
Thu, 10 Aug 2017 00:58:13 +0000 (20:58 -0400)
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.

Lib/idlelib/calltips.py
Lib/idlelib/idle_test/test_calltips.py
Misc/NEWS.d/next/IDLE/2017-08-03-14-08-42.bpo-19903.sqE1FS.rst [new file with mode: 0644]

index a8a3abe6c6982a8a7db1c5f991cbe93f4a931da5..49625eac158c03aa271c1bf0d67918abb1614cc3 100644 (file)
@@ -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)
index 29b9f06faf868ba4f6879dbb4902ae50d9502b76..fa92ece78ee6b9315a14c90ab7a97bfd169e8c12 100644 (file)
@@ -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,28 @@ class Get_signatureTest(unittest.TestCase):
             self.assertEqual(signature(obj), out)
 
         if List.__doc__ is not None:
-            gtest(List, List.__doc__)
+            gtest(List, '(iterable=(), /)' + ct._argument_positional + '\n' +
+                  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__,
+               '(self, /, *args, **kwargs)' + 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 + "\nAppend object to the end of the list."
+        gtest(list.append, '(self, object, /)' + append_doc)
+        gtest(List.append, '(self, object, /)' + append_doc)
+        gtest([].append, '(object, /)' + 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 +143,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 +175,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 (file)
index 0000000..f25fc80
--- /dev/null
@@ -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.