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.
This commit is contained in:
Serhiy Storchaka 2023-08-11 20:51:36 +03:00 committed by GitHub
parent 5f7d4ecf30
commit a39f0a3506
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 94 additions and 40 deletions

View File

@ -197,6 +197,24 @@ def splitdoc(doc):
return lines[0], '\n'.join(lines[2:]) return lines[0], '\n'.join(lines[2:])
return '', '\n'.join(lines) 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): def classname(object, modname):
"""Get a class name and qualify it with a module name if necessary.""" """Get a class name and qualify it with a module name if necessary."""
name = object.__name__ name = object.__name__
@ -1003,14 +1021,9 @@ class HTMLDoc(Doc):
title = title + '(%s)' % ', '.join(parents) title = title + '(%s)' % ', '.join(parents)
decl = '' decl = ''
try: argspec = _getargspec(object)
signature = inspect.signature(object) if argspec and argspec != '()':
except (ValueError, TypeError): decl = name + self.escape(argspec) + '\n\n'
signature = None
if signature:
argspec = str(signature)
if argspec and argspec != '()':
decl = name + self.escape(argspec) + '\n\n'
doc = getdoc(object) doc = getdoc(object)
if decl: if decl:
@ -1063,18 +1076,13 @@ class HTMLDoc(Doc):
anchor, name, reallink) anchor, name, reallink)
argspec = None argspec = None
if inspect.isroutine(object): if inspect.isroutine(object):
try: argspec = _getargspec(object)
signature = inspect.signature(object) if argspec and realname == '<lambda>':
except (ValueError, TypeError): title = '<strong>%s</strong> <em>lambda</em> ' % name
signature = None # XXX lambda's won't usually have func_annotations['return']
if signature: # since the syntax doesn't support but it is possible.
argspec = str(signature) # So removing parentheses isn't truly safe.
if realname == '<lambda>': argspec = argspec[1:-1] # remove parentheses
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: if not argspec:
argspec = '(...)' argspec = '(...)'
@ -1321,14 +1329,9 @@ location listed above.
contents = [] contents = []
push = contents.append push = contents.append
try: argspec = _getargspec(object)
signature = inspect.signature(object) if argspec and argspec != '()':
except (ValueError, TypeError): push(name + argspec + '\n')
signature = None
if signature:
argspec = str(signature)
if argspec and argspec != '()':
push(name + argspec + '\n')
doc = getdoc(object) doc = getdoc(object)
if doc: if doc:
@ -1492,18 +1495,13 @@ location listed above.
argspec = None argspec = None
if inspect.isroutine(object): if inspect.isroutine(object):
try: argspec = _getargspec(object)
signature = inspect.signature(object) if argspec and realname == '<lambda>':
except (ValueError, TypeError): title = self.bold(name) + ' lambda '
signature = None # XXX lambda's won't usually have func_annotations['return']
if signature: # since the syntax doesn't support but it is possible.
argspec = str(signature) # So removing parentheses isn't truly safe.
if realname == '<lambda>': argspec = argspec[1:-1] # remove parentheses
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: if not argspec:
argspec = '(...)' argspec = '(...)'
decl = asyncqualifier + title + argspec + note decl = asyncqualifier + title + argspec + note

View File

@ -1230,6 +1230,60 @@ class TestDescriptions(unittest.TestCase):
self.assertEqual(self._get_summary_line(dict.__class_getitem__), self.assertEqual(self._get_summary_line(dict.__class_getitem__),
"__class_getitem__(object, /) method of builtins.type instance") "__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 @requires_docstrings
def test_staticmethod(self): def test_staticmethod(self):
class X: class X:

View File

@ -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``.