[Python-checkins] gh-107782: Pydoc: fall back to __text_signature__ if inspect.signature() fails (GH-107786)

serhiy-storchaka webhook-mailer at python.org
Fri Aug 11 13:51:40 EDT 2023


https://github.com/python/cpython/commit/a39f0a350662f1978104ee1136472d784aa6f29c
commit: a39f0a350662f1978104ee1136472d784aa6f29c
branch: main
author: Serhiy Storchaka <storchaka at gmail.com>
committer: serhiy-storchaka <storchaka at gmail.com>
date: 2023-08-11T20:51:36+03:00
summary:

gh-107782: Pydoc: fall back to __text_signature__ if inspect.signature() fails (GH-107786)

It allows to show signatures which are not representable in Python,
e.g. for getattr and dict.pop.

files:
A Misc/NEWS.d/next/Library/2023-08-08-19-57-45.gh-issue-107782.mInjFE.rst
M Lib/pydoc.py
M Lib/test/test_pydoc.py

diff --git a/Lib/pydoc.py b/Lib/pydoc.py
index 185f09e603df2..c9a55799b39f0 100755
--- a/Lib/pydoc.py
+++ b/Lib/pydoc.py
@@ -197,6 +197,24 @@ def splitdoc(doc):
         return lines[0], '\n'.join(lines[2:])
     return '', '\n'.join(lines)
 
+def _getargspec(object):
+    try:
+        signature = inspect.signature(object)
+        if signature:
+            return str(signature)
+    except (ValueError, TypeError):
+        argspec = getattr(object, '__text_signature__', None)
+        if argspec:
+            if argspec[:2] == '($':
+                argspec = '(' + argspec[2:]
+            if getattr(object, '__self__', None) is not None:
+                # Strip the bound argument.
+                m = re.match(r'\(\w+(?:(?=\))|,\s*(?:/(?:(?=\))|,\s*))?)', argspec)
+                if m:
+                    argspec = '(' + argspec[m.end():]
+        return argspec
+    return None
+
 def classname(object, modname):
     """Get a class name and qualify it with a module name if necessary."""
     name = object.__name__
@@ -1003,14 +1021,9 @@ def spilldata(msg, attrs, predicate):
             title = title + '(%s)' % ', '.join(parents)
 
         decl = ''
-        try:
-            signature = inspect.signature(object)
-        except (ValueError, TypeError):
-            signature = None
-        if signature:
-            argspec = str(signature)
-            if argspec and argspec != '()':
-                decl = name + self.escape(argspec) + '\n\n'
+        argspec = _getargspec(object)
+        if argspec and argspec != '()':
+            decl = name + self.escape(argspec) + '\n\n'
 
         doc = getdoc(object)
         if decl:
@@ -1063,18 +1076,13 @@ def docroutine(self, object, name=None, mod=None,
                 anchor, name, reallink)
         argspec = None
         if inspect.isroutine(object):
-            try:
-                signature = inspect.signature(object)
-            except (ValueError, TypeError):
-                signature = None
-            if signature:
-                argspec = str(signature)
-                if realname == '<lambda>':
-                    title = '<strong>%s</strong> <em>lambda</em> ' % name
-                    # XXX lambda's won't usually have func_annotations['return']
-                    # since the syntax doesn't support but it is possible.
-                    # So removing parentheses isn't truly safe.
-                    argspec = argspec[1:-1] # remove parentheses
+            argspec = _getargspec(object)
+            if argspec and realname == '<lambda>':
+                title = '<strong>%s</strong> <em>lambda</em> ' % name
+                # XXX lambda's won't usually have func_annotations['return']
+                # since the syntax doesn't support but it is possible.
+                # So removing parentheses isn't truly safe.
+                argspec = argspec[1:-1] # remove parentheses
         if not argspec:
             argspec = '(...)'
 
@@ -1321,14 +1329,9 @@ def makename(c, m=object.__module__):
         contents = []
         push = contents.append
 
-        try:
-            signature = inspect.signature(object)
-        except (ValueError, TypeError):
-            signature = None
-        if signature:
-            argspec = str(signature)
-            if argspec and argspec != '()':
-                push(name + argspec + '\n')
+        argspec = _getargspec(object)
+        if argspec and argspec != '()':
+            push(name + argspec + '\n')
 
         doc = getdoc(object)
         if doc:
@@ -1492,18 +1495,13 @@ def docroutine(self, object, name=None, mod=None, cl=None):
         argspec = None
 
         if inspect.isroutine(object):
-            try:
-                signature = inspect.signature(object)
-            except (ValueError, TypeError):
-                signature = None
-            if signature:
-                argspec = str(signature)
-                if realname == '<lambda>':
-                    title = self.bold(name) + ' lambda '
-                    # XXX lambda's won't usually have func_annotations['return']
-                    # since the syntax doesn't support but it is possible.
-                    # So removing parentheses isn't truly safe.
-                    argspec = argspec[1:-1] # remove parentheses
+            argspec = _getargspec(object)
+            if argspec and realname == '<lambda>':
+                title = self.bold(name) + ' lambda '
+                # XXX lambda's won't usually have func_annotations['return']
+                # since the syntax doesn't support but it is possible.
+                # So removing parentheses isn't truly safe.
+                argspec = argspec[1:-1] # remove parentheses
         if not argspec:
             argspec = '(...)'
         decl = asyncqualifier + title + argspec + note
diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py
index 8df8b608cf959..fe4e37d4858c8 100644
--- a/Lib/test/test_pydoc.py
+++ b/Lib/test/test_pydoc.py
@@ -1230,6 +1230,60 @@ def test_bound_builtin_classmethod_o(self):
         self.assertEqual(self._get_summary_line(dict.__class_getitem__),
             "__class_getitem__(object, /) method of builtins.type instance")
 
+    def test_module_level_callable_unrepresentable_default(self):
+        self.assertEqual(self._get_summary_line(getattr),
+            "getattr(object, name, default=<unrepresentable>, /)")
+
+    def test_builtin_staticmethod_unrepresentable_default(self):
+        self.assertEqual(self._get_summary_line(str.maketrans),
+            "maketrans(x, y=<unrepresentable>, z=<unrepresentable>, /)")
+
+    def test_unbound_builtin_method_unrepresentable_default(self):
+        self.assertEqual(self._get_summary_line(dict.pop),
+            "pop(self, key, default=<unrepresentable>, /)")
+
+    def test_bound_builtin_method_unrepresentable_default(self):
+        self.assertEqual(self._get_summary_line({}.pop),
+            "pop(key, default=<unrepresentable>, /) "
+            "method of builtins.dict instance")
+
+    def test_overridden_text_signature(self):
+        class C:
+            def meth(*args, **kwargs):
+                pass
+            @classmethod
+            def cmeth(*args, **kwargs):
+                pass
+            @staticmethod
+            def smeth(*args, **kwargs):
+                pass
+        for text_signature, unbound, bound in [
+            ("($slf)", "(slf, /)", "()"),
+            ("($slf, /)", "(slf, /)", "()"),
+            ("($slf, /, arg)", "(slf, /, arg)", "(arg)"),
+            ("($slf, /, arg=<x>)", "(slf, /, arg=<x>)", "(arg=<x>)"),
+            ("($slf, arg, /)", "(slf, arg, /)", "(arg, /)"),
+            ("($slf, arg=<x>, /)", "(slf, arg=<x>, /)", "(arg=<x>, /)"),
+            ("(/, slf, arg)", "(/, slf, arg)", "(/, slf, arg)"),
+            ("(/, slf, arg=<x>)", "(/, slf, arg=<x>)", "(/, slf, arg=<x>)"),
+            ("(slf, /, arg)", "(slf, /, arg)", "(arg)"),
+            ("(slf, /, arg=<x>)", "(slf, /, arg=<x>)", "(arg=<x>)"),
+            ("(slf, arg, /)", "(slf, arg, /)", "(arg, /)"),
+            ("(slf, arg=<x>, /)", "(slf, arg=<x>, /)", "(arg=<x>, /)"),
+        ]:
+            with self.subTest(text_signature):
+                C.meth.__text_signature__ = text_signature
+                self.assertEqual(self._get_summary_line(C.meth),
+                        "meth" + unbound)
+                self.assertEqual(self._get_summary_line(C().meth),
+                        "meth" + bound + " method of test.test_pydoc.C instance")
+                C.cmeth.__func__.__text_signature__ = text_signature
+                self.assertEqual(self._get_summary_line(C.cmeth),
+                        "cmeth" + bound + " method of builtins.type instance")
+                C.smeth.__text_signature__ = text_signature
+                self.assertEqual(self._get_summary_line(C.smeth),
+                        "smeth" + unbound)
+
     @requires_docstrings
     def test_staticmethod(self):
         class X:
diff --git a/Misc/NEWS.d/next/Library/2023-08-08-19-57-45.gh-issue-107782.mInjFE.rst b/Misc/NEWS.d/next/Library/2023-08-08-19-57-45.gh-issue-107782.mInjFE.rst
new file mode 100644
index 0000000000000..fb8a50de3a9ee
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-08-08-19-57-45.gh-issue-107782.mInjFE.rst
@@ -0,0 +1,2 @@
+:mod:`pydoc` is now able to show signatures which are not representable in
+Python, e.g. for ``getattr`` and ``dict.pop``.



More information about the Python-checkins mailing list