Skip to content

Commit b9e9003

Browse files
[3.11] [3.12] gh-97959: Fix rendering of routines in pydoc (GH-113941) (GH-115296) (GH-115302)
* Class methods no longer have "method of builtins.type instance" note. * Corresponding notes are now added for class and unbound methods. * Method and function aliases now have references to the module or the class where the origin was defined if it differs from the current. * Bound methods are now listed in the static methods section. * Methods of builtin classes are now supported as well as methods of Python classes. (cherry picked from commit 2939ad0) (cherry picked from commit cfb79ca)
1 parent 83a69a6 commit b9e9003

File tree

5 files changed

+322
-52
lines changed

5 files changed

+322
-52
lines changed

Lib/pydoc.py

Lines changed: 115 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,19 @@ def classname(object, modname):
204204
name = object.__module__ + '.' + name
205205
return name
206206

207+
def parentname(object, modname):
208+
"""Get a name of the enclosing class (qualified it with a module name
209+
if necessary) or module."""
210+
if '.' in object.__qualname__:
211+
name = object.__qualname__.rpartition('.')[0]
212+
if object.__module__ != modname:
213+
return object.__module__ + '.' + name
214+
else:
215+
return name
216+
else:
217+
if object.__module__ != modname:
218+
return object.__module__
219+
207220
def isdata(object):
208221
"""Check if an object is of a type that probably means it's data."""
209222
return not (inspect.ismodule(object) or inspect.isclass(object) or
@@ -298,13 +311,15 @@ def visiblename(name, all=None, obj=None):
298311
return not name.startswith('_')
299312

300313
def classify_class_attrs(object):
301-
"""Wrap inspect.classify_class_attrs, with fixup for data descriptors."""
314+
"""Wrap inspect.classify_class_attrs, with fixup for data descriptors and bound methods."""
302315
results = []
303316
for (name, kind, cls, value) in inspect.classify_class_attrs(object):
304317
if inspect.isdatadescriptor(value):
305318
kind = 'data descriptor'
306319
if isinstance(value, property) and value.fset is None:
307320
kind = 'readonly property'
321+
elif kind == 'method' and _is_bound_method(value):
322+
kind = 'static method'
308323
results.append((name, kind, cls, value))
309324
return results
310325

@@ -653,6 +668,25 @@ def classlink(self, object, modname):
653668
module.__name__, name, classname(object, modname))
654669
return classname(object, modname)
655670

671+
def parentlink(self, object, modname):
672+
"""Make a link for the enclosing class or module."""
673+
link = None
674+
name, module = object.__name__, sys.modules.get(object.__module__)
675+
if hasattr(module, name) and getattr(module, name) is object:
676+
if '.' in object.__qualname__:
677+
name = object.__qualname__.rpartition('.')[0]
678+
if object.__module__ != modname:
679+
link = '%s.html#%s' % (module.__name__, name)
680+
else:
681+
link = '#%s' % name
682+
else:
683+
if object.__module__ != modname:
684+
link = '%s.html' % module.__name__
685+
if link:
686+
return '<a href="%s">%s</a>' % (link, parentname(object, modname))
687+
else:
688+
return parentname(object, modname)
689+
656690
def modulelink(self, object):
657691
"""Make a link for a module."""
658692
return '<a href="%s.html">%s</a>' % (object.__name__, object.__name__)
@@ -899,7 +933,7 @@ def spill(msg, attrs, predicate):
899933
push(self.docdata(value, name, mod))
900934
else:
901935
push(self.document(value, name, mod,
902-
funcs, classes, mdict, object))
936+
funcs, classes, mdict, object, homecls))
903937
push('\n')
904938
return attrs
905939

@@ -1022,24 +1056,44 @@ def formatvalue(self, object):
10221056
return self.grey('=' + self.repr(object))
10231057

10241058
def docroutine(self, object, name=None, mod=None,
1025-
funcs={}, classes={}, methods={}, cl=None):
1059+
funcs={}, classes={}, methods={}, cl=None, homecls=None):
10261060
"""Produce HTML documentation for a function or method object."""
10271061
realname = object.__name__
10281062
name = name or realname
1029-
anchor = (cl and cl.__name__ or '') + '-' + name
1063+
if homecls is None:
1064+
homecls = cl
1065+
anchor = ('' if cl is None else cl.__name__) + '-' + name
10301066
note = ''
1031-
skipdocs = 0
1067+
skipdocs = False
1068+
imfunc = None
10321069
if _is_bound_method(object):
1033-
imclass = object.__self__.__class__
1034-
if cl:
1035-
if imclass is not cl:
1036-
note = ' from ' + self.classlink(imclass, mod)
1070+
imself = object.__self__
1071+
if imself is cl:
1072+
imfunc = getattr(object, '__func__', None)
1073+
elif inspect.isclass(imself):
1074+
note = ' class method of %s' % self.classlink(imself, mod)
10371075
else:
1038-
if object.__self__ is not None:
1039-
note = ' method of %s instance' % self.classlink(
1040-
object.__self__.__class__, mod)
1041-
else:
1042-
note = ' unbound %s method' % self.classlink(imclass,mod)
1076+
note = ' method of %s instance' % self.classlink(
1077+
imself.__class__, mod)
1078+
elif (inspect.ismethoddescriptor(object) or
1079+
inspect.ismethodwrapper(object)):
1080+
try:
1081+
objclass = object.__objclass__
1082+
except AttributeError:
1083+
pass
1084+
else:
1085+
if cl is None:
1086+
note = ' unbound %s method' % self.classlink(objclass, mod)
1087+
elif objclass is not homecls:
1088+
note = ' from ' + self.classlink(objclass, mod)
1089+
else:
1090+
imfunc = object
1091+
if inspect.isfunction(imfunc) and homecls is not None and (
1092+
imfunc.__module__ != homecls.__module__ or
1093+
imfunc.__qualname__ != homecls.__qualname__ + '.' + realname):
1094+
pname = self.parentlink(imfunc, mod)
1095+
if pname:
1096+
note = ' from %s' % pname
10431097

10441098
if (inspect.iscoroutinefunction(object) or
10451099
inspect.isasyncgenfunction(object)):
@@ -1050,10 +1104,13 @@ def docroutine(self, object, name=None, mod=None,
10501104
if name == realname:
10511105
title = '<a name="%s"><strong>%s</strong></a>' % (anchor, realname)
10521106
else:
1053-
if cl and inspect.getattr_static(cl, realname, []) is object:
1107+
if (cl is not None and
1108+
inspect.getattr_static(cl, realname, []) is object):
10541109
reallink = '<a href="#%s">%s</a>' % (
10551110
cl.__name__ + '-' + realname, realname)
1056-
skipdocs = 1
1111+
skipdocs = True
1112+
if note.startswith(' from '):
1113+
note = ''
10571114
else:
10581115
reallink = realname
10591116
title = '<a name="%s"><strong>%s</strong></a> = %s' % (
@@ -1086,7 +1143,7 @@ def docroutine(self, object, name=None, mod=None,
10861143
doc = doc and '<dd><span class="code">%s</span></dd>' % doc
10871144
return '<dl><dt>%s</dt>%s</dl>\n' % (decl, doc)
10881145

1089-
def docdata(self, object, name=None, mod=None, cl=None):
1146+
def docdata(self, object, name=None, mod=None, cl=None, *ignored):
10901147
"""Produce html documentation for a data descriptor."""
10911148
results = []
10921149
push = results.append
@@ -1198,7 +1255,7 @@ def formattree(self, tree, modname, parent=None, prefix=''):
11981255
entry, modname, c, prefix + ' ')
11991256
return result
12001257

1201-
def docmodule(self, object, name=None, mod=None):
1258+
def docmodule(self, object, name=None, mod=None, *ignored):
12021259
"""Produce text documentation for a given module object."""
12031260
name = object.__name__ # ignore the passed-in name
12041261
synop, desc = splitdoc(getdoc(object))
@@ -1382,7 +1439,7 @@ def spill(msg, attrs, predicate):
13821439
push(self.docdata(value, name, mod))
13831440
else:
13841441
push(self.document(value,
1385-
name, mod, object))
1442+
name, mod, object, homecls))
13861443
return attrs
13871444

13881445
def spilldescriptors(msg, attrs, predicate):
@@ -1457,23 +1514,43 @@ def formatvalue(self, object):
14571514
"""Format an argument default value as text."""
14581515
return '=' + self.repr(object)
14591516

1460-
def docroutine(self, object, name=None, mod=None, cl=None):
1517+
def docroutine(self, object, name=None, mod=None, cl=None, homecls=None):
14611518
"""Produce text documentation for a function or method object."""
14621519
realname = object.__name__
14631520
name = name or realname
1521+
if homecls is None:
1522+
homecls = cl
14641523
note = ''
1465-
skipdocs = 0
1524+
skipdocs = False
1525+
imfunc = None
14661526
if _is_bound_method(object):
1467-
imclass = object.__self__.__class__
1468-
if cl:
1469-
if imclass is not cl:
1470-
note = ' from ' + classname(imclass, mod)
1527+
imself = object.__self__
1528+
if imself is cl:
1529+
imfunc = getattr(object, '__func__', None)
1530+
elif inspect.isclass(imself):
1531+
note = ' class method of %s' % classname(imself, mod)
14711532
else:
1472-
if object.__self__ is not None:
1473-
note = ' method of %s instance' % classname(
1474-
object.__self__.__class__, mod)
1475-
else:
1476-
note = ' unbound %s method' % classname(imclass,mod)
1533+
note = ' method of %s instance' % classname(
1534+
imself.__class__, mod)
1535+
elif (inspect.ismethoddescriptor(object) or
1536+
inspect.ismethodwrapper(object)):
1537+
try:
1538+
objclass = object.__objclass__
1539+
except AttributeError:
1540+
pass
1541+
else:
1542+
if cl is None:
1543+
note = ' unbound %s method' % classname(objclass, mod)
1544+
elif objclass is not homecls:
1545+
note = ' from ' + classname(objclass, mod)
1546+
else:
1547+
imfunc = object
1548+
if inspect.isfunction(imfunc) and homecls is not None and (
1549+
imfunc.__module__ != homecls.__module__ or
1550+
imfunc.__qualname__ != homecls.__qualname__ + '.' + realname):
1551+
pname = parentname(imfunc, mod)
1552+
if pname:
1553+
note = ' from %s' % pname
14771554

14781555
if (inspect.iscoroutinefunction(object) or
14791556
inspect.isasyncgenfunction(object)):
@@ -1484,8 +1561,11 @@ def docroutine(self, object, name=None, mod=None, cl=None):
14841561
if name == realname:
14851562
title = self.bold(realname)
14861563
else:
1487-
if cl and inspect.getattr_static(cl, realname, []) is object:
1488-
skipdocs = 1
1564+
if (cl is not None and
1565+
inspect.getattr_static(cl, realname, []) is object):
1566+
skipdocs = True
1567+
if note.startswith(' from '):
1568+
note = ''
14891569
title = self.bold(name) + ' = ' + realname
14901570
argspec = None
14911571

@@ -1512,7 +1592,7 @@ def docroutine(self, object, name=None, mod=None, cl=None):
15121592
doc = getdoc(object) or ''
15131593
return decl + '\n' + (doc and self.indent(doc).rstrip() + '\n')
15141594

1515-
def docdata(self, object, name=None, mod=None, cl=None):
1595+
def docdata(self, object, name=None, mod=None, cl=None, *ignored):
15161596
"""Produce text documentation for a data descriptor."""
15171597
results = []
15181598
push = results.append
@@ -1528,7 +1608,8 @@ def docdata(self, object, name=None, mod=None, cl=None):
15281608

15291609
docproperty = docdata
15301610

1531-
def docother(self, object, name=None, mod=None, parent=None, maxlen=None, doc=None):
1611+
def docother(self, object, name=None, mod=None, parent=None, *ignored,
1612+
maxlen=None, doc=None):
15321613
"""Produce text documentation for a data object."""
15331614
repr = self.repr(object)
15341615
if maxlen:

Lib/test/pydocfodder.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
import types
44

5+
def global_func(x, y):
6+
"""Module global function"""
7+
8+
def global_func2(x, y):
9+
"""Module global function 2"""
10+
511
class A:
612
"A class."
713

@@ -26,7 +32,7 @@ def A_classmethod(cls, x):
2632
"A class method defined in A."
2733
A_classmethod = classmethod(A_classmethod)
2834

29-
def A_staticmethod():
35+
def A_staticmethod(x, y):
3036
"A static method defined in A."
3137
A_staticmethod = staticmethod(A_staticmethod)
3238

@@ -61,6 +67,28 @@ def BD_method(self):
6167
def BCD_method(self):
6268
"Method defined in B, C and D."
6369

70+
@classmethod
71+
def B_classmethod(cls, x):
72+
"A class method defined in B."
73+
74+
global_func = global_func # same name
75+
global_func_alias = global_func
76+
global_func2_alias = global_func2
77+
B_classmethod_alias = B_classmethod
78+
A_classmethod_ref = A.A_classmethod
79+
A_staticmethod = A.A_staticmethod # same name
80+
A_staticmethod_alias = A.A_staticmethod
81+
A_method_ref = A().A_method
82+
A_method_alias = A.A_method
83+
B_method_alias = B_method
84+
__repr__ = object.__repr__ # same name
85+
object_repr = object.__repr__
86+
get = {}.get # same name
87+
dict_get = {}.get
88+
89+
B.B_classmethod_ref = B.B_classmethod
90+
91+
6492
class C(A):
6593
"A class, derived from A."
6694

@@ -136,3 +164,21 @@ def __call__(self, inst):
136164

137165
submodule = types.ModuleType(__name__ + '.submodule',
138166
"""A submodule, which should appear in its parent's summary""")
167+
168+
global_func_alias = global_func
169+
A_classmethod = A.A_classmethod # same name
170+
A_classmethod2 = A.A_classmethod
171+
A_classmethod3 = B.A_classmethod
172+
A_staticmethod = A.A_staticmethod # same name
173+
A_staticmethod_alias = A.A_staticmethod
174+
A_staticmethod_ref = A().A_staticmethod
175+
A_staticmethod_ref2 = B().A_staticmethod
176+
A_method = A().A_method # same name
177+
A_method2 = A().A_method
178+
A_method3 = B().A_method
179+
B_method = B.B_method # same name
180+
B_method2 = B.B_method
181+
count = list.count # same name
182+
list_count = list.count
183+
get = {}.get # same name
184+
dict_get = {}.get

Lib/test/test_enum.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4443,22 +4443,22 @@ class Color(enum.Enum)
44434443
| The value of the Enum member.
44444444
|\x20\x20
44454445
| ----------------------------------------------------------------------
4446-
| Methods inherited from enum.EnumType:
4446+
| Static methods inherited from enum.EnumType:
44474447
|\x20\x20
4448-
| __contains__(member) from enum.EnumType
4448+
| __contains__(member)
44494449
| Return True if member is a member of this enum
44504450
| raises TypeError if member is not an enum member
44514451
|\x20\x20\x20\x20\x20\x20
44524452
| note: in 3.12 TypeError will no longer be raised, and True will also be
44534453
| returned if member is the value of a member in this enum
44544454
|\x20\x20
4455-
| __getitem__(name) from enum.EnumType
4455+
| __getitem__(name)
44564456
| Return the member matching `name`.
44574457
|\x20\x20
4458-
| __iter__() from enum.EnumType
4458+
| __iter__()
44594459
| Return members in definition order.
44604460
|\x20\x20
4461-
| __len__() from enum.EnumType
4461+
| __len__()
44624462
| Return the number of members (no aliases)
44634463
|\x20\x20
44644464
| ----------------------------------------------------------------------

0 commit comments

Comments
 (0)