Skip to content

Commit a39f0a3

Browse files
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.
1 parent 5f7d4ec commit a39f0a3

File tree

3 files changed

+94
-40
lines changed

3 files changed

+94
-40
lines changed

Lib/pydoc.py

+38-40
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,24 @@ def splitdoc(doc):
197197
return lines[0], '\n'.join(lines[2:])
198198
return '', '\n'.join(lines)
199199

200+
def _getargspec(object):
201+
try:
202+
signature = inspect.signature(object)
203+
if signature:
204+
return str(signature)
205+
except (ValueError, TypeError):
206+
argspec = getattr(object, '__text_signature__', None)
207+
if argspec:
208+
if argspec[:2] == '($':
209+
argspec = '(' + argspec[2:]
210+
if getattr(object, '__self__', None) is not None:
211+
# Strip the bound argument.
212+
m = re.match(r'\(\w+(?:(?=\))|,\s*(?:/(?:(?=\))|,\s*))?)', argspec)
213+
if m:
214+
argspec = '(' + argspec[m.end():]
215+
return argspec
216+
return None
217+
200218
def classname(object, modname):
201219
"""Get a class name and qualify it with a module name if necessary."""
202220
name = object.__name__
@@ -1003,14 +1021,9 @@ def spilldata(msg, attrs, predicate):
10031021
title = title + '(%s)' % ', '.join(parents)
10041022

10051023
decl = ''
1006-
try:
1007-
signature = inspect.signature(object)
1008-
except (ValueError, TypeError):
1009-
signature = None
1010-
if signature:
1011-
argspec = str(signature)
1012-
if argspec and argspec != '()':
1013-
decl = name + self.escape(argspec) + '\n\n'
1024+
argspec = _getargspec(object)
1025+
if argspec and argspec != '()':
1026+
decl = name + self.escape(argspec) + '\n\n'
10141027

10151028
doc = getdoc(object)
10161029
if decl:
@@ -1063,18 +1076,13 @@ def docroutine(self, object, name=None, mod=None,
10631076
anchor, name, reallink)
10641077
argspec = None
10651078
if inspect.isroutine(object):
1066-
try:
1067-
signature = inspect.signature(object)
1068-
except (ValueError, TypeError):
1069-
signature = None
1070-
if signature:
1071-
argspec = str(signature)
1072-
if realname == '<lambda>':
1073-
title = '<strong>%s</strong> <em>lambda</em> ' % name
1074-
# XXX lambda's won't usually have func_annotations['return']
1075-
# since the syntax doesn't support but it is possible.
1076-
# So removing parentheses isn't truly safe.
1077-
argspec = argspec[1:-1] # remove parentheses
1079+
argspec = _getargspec(object)
1080+
if argspec and realname == '<lambda>':
1081+
title = '<strong>%s</strong> <em>lambda</em> ' % name
1082+
# XXX lambda's won't usually have func_annotations['return']
1083+
# since the syntax doesn't support but it is possible.
1084+
# So removing parentheses isn't truly safe.
1085+
argspec = argspec[1:-1] # remove parentheses
10781086
if not argspec:
10791087
argspec = '(...)'
10801088

@@ -1321,14 +1329,9 @@ def makename(c, m=object.__module__):
13211329
contents = []
13221330
push = contents.append
13231331

1324-
try:
1325-
signature = inspect.signature(object)
1326-
except (ValueError, TypeError):
1327-
signature = None
1328-
if signature:
1329-
argspec = str(signature)
1330-
if argspec and argspec != '()':
1331-
push(name + argspec + '\n')
1332+
argspec = _getargspec(object)
1333+
if argspec and argspec != '()':
1334+
push(name + argspec + '\n')
13321335

13331336
doc = getdoc(object)
13341337
if doc:
@@ -1492,18 +1495,13 @@ def docroutine(self, object, name=None, mod=None, cl=None):
14921495
argspec = None
14931496

14941497
if inspect.isroutine(object):
1495-
try:
1496-
signature = inspect.signature(object)
1497-
except (ValueError, TypeError):
1498-
signature = None
1499-
if signature:
1500-
argspec = str(signature)
1501-
if realname == '<lambda>':
1502-
title = self.bold(name) + ' lambda '
1503-
# XXX lambda's won't usually have func_annotations['return']
1504-
# since the syntax doesn't support but it is possible.
1505-
# So removing parentheses isn't truly safe.
1506-
argspec = argspec[1:-1] # remove parentheses
1498+
argspec = _getargspec(object)
1499+
if argspec and realname == '<lambda>':
1500+
title = self.bold(name) + ' lambda '
1501+
# XXX lambda's won't usually have func_annotations['return']
1502+
# since the syntax doesn't support but it is possible.
1503+
# So removing parentheses isn't truly safe.
1504+
argspec = argspec[1:-1] # remove parentheses
15071505
if not argspec:
15081506
argspec = '(...)'
15091507
decl = asyncqualifier + title + argspec + note

Lib/test/test_pydoc.py

+54
Original file line numberDiff line numberDiff line change
@@ -1230,6 +1230,60 @@ def test_bound_builtin_classmethod_o(self):
12301230
self.assertEqual(self._get_summary_line(dict.__class_getitem__),
12311231
"__class_getitem__(object, /) method of builtins.type instance")
12321232

1233+
def test_module_level_callable_unrepresentable_default(self):
1234+
self.assertEqual(self._get_summary_line(getattr),
1235+
"getattr(object, name, default=<unrepresentable>, /)")
1236+
1237+
def test_builtin_staticmethod_unrepresentable_default(self):
1238+
self.assertEqual(self._get_summary_line(str.maketrans),
1239+
"maketrans(x, y=<unrepresentable>, z=<unrepresentable>, /)")
1240+
1241+
def test_unbound_builtin_method_unrepresentable_default(self):
1242+
self.assertEqual(self._get_summary_line(dict.pop),
1243+
"pop(self, key, default=<unrepresentable>, /)")
1244+
1245+
def test_bound_builtin_method_unrepresentable_default(self):
1246+
self.assertEqual(self._get_summary_line({}.pop),
1247+
"pop(key, default=<unrepresentable>, /) "
1248+
"method of builtins.dict instance")
1249+
1250+
def test_overridden_text_signature(self):
1251+
class C:
1252+
def meth(*args, **kwargs):
1253+
pass
1254+
@classmethod
1255+
def cmeth(*args, **kwargs):
1256+
pass
1257+
@staticmethod
1258+
def smeth(*args, **kwargs):
1259+
pass
1260+
for text_signature, unbound, bound in [
1261+
("($slf)", "(slf, /)", "()"),
1262+
("($slf, /)", "(slf, /)", "()"),
1263+
("($slf, /, arg)", "(slf, /, arg)", "(arg)"),
1264+
("($slf, /, arg=<x>)", "(slf, /, arg=<x>)", "(arg=<x>)"),
1265+
("($slf, arg, /)", "(slf, arg, /)", "(arg, /)"),
1266+
("($slf, arg=<x>, /)", "(slf, arg=<x>, /)", "(arg=<x>, /)"),
1267+
("(/, slf, arg)", "(/, slf, arg)", "(/, slf, arg)"),
1268+
("(/, slf, arg=<x>)", "(/, slf, arg=<x>)", "(/, slf, arg=<x>)"),
1269+
("(slf, /, arg)", "(slf, /, arg)", "(arg)"),
1270+
("(slf, /, arg=<x>)", "(slf, /, arg=<x>)", "(arg=<x>)"),
1271+
("(slf, arg, /)", "(slf, arg, /)", "(arg, /)"),
1272+
("(slf, arg=<x>, /)", "(slf, arg=<x>, /)", "(arg=<x>, /)"),
1273+
]:
1274+
with self.subTest(text_signature):
1275+
C.meth.__text_signature__ = text_signature
1276+
self.assertEqual(self._get_summary_line(C.meth),
1277+
"meth" + unbound)
1278+
self.assertEqual(self._get_summary_line(C().meth),
1279+
"meth" + bound + " method of test.test_pydoc.C instance")
1280+
C.cmeth.__func__.__text_signature__ = text_signature
1281+
self.assertEqual(self._get_summary_line(C.cmeth),
1282+
"cmeth" + bound + " method of builtins.type instance")
1283+
C.smeth.__text_signature__ = text_signature
1284+
self.assertEqual(self._get_summary_line(C.smeth),
1285+
"smeth" + unbound)
1286+
12331287
@requires_docstrings
12341288
def test_staticmethod(self):
12351289
class X:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:mod:`pydoc` is now able to show signatures which are not representable in
2+
Python, e.g. for ``getattr`` and ``dict.pop``.

0 commit comments

Comments
 (0)