summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSerhiy Storchaka <storchaka@gmail.com>2023-08-11 17:51:36 (GMT)
committerGitHub <noreply@github.com>2023-08-11 17:51:36 (GMT)
commita39f0a350662f1978104ee1136472d784aa6f29c (patch)
treea13f5ab25aa3b11743d7d146524887dee88c1805
parent5f7d4ecf301ef12eb1d1d347add054f4fcd8fc5c (diff)
downloadcpython-a39f0a350662f1978104ee1136472d784aa6f29c.zip
cpython-a39f0a350662f1978104ee1136472d784aa6f29c.tar.gz
cpython-a39f0a350662f1978104ee1136472d784aa6f29c.tar.bz2
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.
-rwxr-xr-xLib/pydoc.py78
-rw-r--r--Lib/test/test_pydoc.py54
-rw-r--r--Misc/NEWS.d/next/Library/2023-08-08-19-57-45.gh-issue-107782.mInjFE.rst2
3 files changed, 94 insertions, 40 deletions
diff --git a/Lib/pydoc.py b/Lib/pydoc.py
index 185f09e..c9a5579 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 @@ class HTMLDoc(Doc):
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 @@ class HTMLDoc(Doc):
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 @@ location listed above.
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 @@ location listed above.
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 8df8b60..fe4e37d 100644
--- a/Lib/test/test_pydoc.py
+++ b/Lib/test/test_pydoc.py
@@ -1230,6 +1230,60 @@ class TestDescriptions(unittest.TestCase):
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 0000000..fb8a50d
--- /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``.