Skip to content

Commit cfb79ca

Browse files
[3.12] gh-97959: Fix rendering of routines in pydoc (GH-113941) (GH-115296)
* 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)
1 parent d8346d6 commit cfb79ca

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

@@ -658,6 +673,25 @@ def classlink(self, object, modname):
658673
module.__name__, name, classname(object, modname))
659674
return classname(object, modname)
660675

676+
def parentlink(self, object, modname):
677+
"""Make a link for the enclosing class or module."""
678+
link = None
679+
name, module = object.__name__, sys.modules.get(object.__module__)
680+
if hasattr(module, name) and getattr(module, name) is object:
681+
if '.' in object.__qualname__:
682+
name = object.__qualname__.rpartition('.')[0]
683+
if object.__module__ != modname:
684+
link = '%s.html#%s' % (module.__name__, name)
685+
else:
686+
link = '#%s' % name
687+
else:
688+
if object.__module__ != modname:
689+
link = '%s.html' % module.__name__
690+
if link:
691+
return '<a href="%s">%s</a>' % (link, parentname(object, modname))
692+
else:
693+
return parentname(object, modname)
694+
661695
def modulelink(self, object):
662696
"""Make a link for a module."""
663697
return '<a href="%s.html">%s</a>' % (object.__name__, object.__name__)
@@ -902,7 +936,7 @@ def spill(msg, attrs, predicate):
902936
push(self.docdata(value, name, mod))
903937
else:
904938
push(self.document(value, name, mod,
905-
funcs, classes, mdict, object))
939+
funcs, classes, mdict, object, homecls))
906940
push('\n')
907941
return attrs
908942

@@ -1025,24 +1059,44 @@ def formatvalue(self, object):
10251059
return self.grey('=' + self.repr(object))
10261060

10271061
def docroutine(self, object, name=None, mod=None,
1028-
funcs={}, classes={}, methods={}, cl=None):
1062+
funcs={}, classes={}, methods={}, cl=None, homecls=None):
10291063
"""Produce HTML documentation for a function or method object."""
10301064
realname = object.__name__
10311065
name = name or realname
1032-
anchor = (cl and cl.__name__ or '') + '-' + name
1066+
if homecls is None:
1067+
homecls = cl
1068+
anchor = ('' if cl is None else cl.__name__) + '-' + name
10331069
note = ''
1034-
skipdocs = 0
1070+
skipdocs = False
1071+
imfunc = None
10351072
if _is_bound_method(object):
1036-
imclass = object.__self__.__class__
1037-
if cl:
1038-
if imclass is not cl:
1039-
note = ' from ' + self.classlink(imclass, mod)
1073+
imself = object.__self__
1074+
if imself is cl:
1075+
imfunc = getattr(object, '__func__', None)
1076+
elif inspect.isclass(imself):
1077+
note = ' class method of %s' % self.classlink(imself, mod)
10401078
else:
1041-
if object.__self__ is not None:
1042-
note = ' method of %s instance' % self.classlink(
1043-
object.__self__.__class__, mod)
1044-
else:
1045-
note = ' unbound %s method' % self.classlink(imclass,mod)
1079+
note = ' method of %s instance' % self.classlink(
1080+
imself.__class__, mod)
1081+
elif (inspect.ismethoddescriptor(object) or
1082+
inspect.ismethodwrapper(object)):
1083+
try:
1084+
objclass = object.__objclass__
1085+
except AttributeError:
1086+
pass
1087+
else:
1088+
if cl is None:
1089+
note = ' unbound %s method' % self.classlink(objclass, mod)
1090+
elif objclass is not homecls:
1091+
note = ' from ' + self.classlink(objclass, mod)
1092+
else:
1093+
imfunc = object
1094+
if inspect.isfunction(imfunc) and homecls is not None and (
1095+
imfunc.__module__ != homecls.__module__ or
1096+
imfunc.__qualname__ != homecls.__qualname__ + '.' + realname):
1097+
pname = self.parentlink(imfunc, mod)
1098+
if pname:
1099+
note = ' from %s' % pname
10461100

10471101
if (inspect.iscoroutinefunction(object) or
10481102
inspect.isasyncgenfunction(object)):
@@ -1053,10 +1107,13 @@ def docroutine(self, object, name=None, mod=None,
10531107
if name == realname:
10541108
title = '<a name="%s"><strong>%s</strong></a>' % (anchor, realname)
10551109
else:
1056-
if cl and inspect.getattr_static(cl, realname, []) is object:
1110+
if (cl is not None and
1111+
inspect.getattr_static(cl, realname, []) is object):
10571112
reallink = '<a href="#%s">%s</a>' % (
10581113
cl.__name__ + '-' + realname, realname)
1059-
skipdocs = 1
1114+
skipdocs = True
1115+
if note.startswith(' from '):
1116+
note = ''
10601117
else:
10611118
reallink = realname
10621119
title = '<a name="%s"><strong>%s</strong></a> = %s' % (
@@ -1089,7 +1146,7 @@ def docroutine(self, object, name=None, mod=None,
10891146
doc = doc and '<dd><span class="code">%s</span></dd>' % doc
10901147
return '<dl><dt>%s</dt>%s</dl>\n' % (decl, doc)
10911148

1092-
def docdata(self, object, name=None, mod=None, cl=None):
1149+
def docdata(self, object, name=None, mod=None, cl=None, *ignored):
10931150
"""Produce html documentation for a data descriptor."""
10941151
results = []
10951152
push = results.append
@@ -1200,7 +1257,7 @@ def formattree(self, tree, modname, parent=None, prefix=''):
12001257
entry, modname, c, prefix + ' ')
12011258
return result
12021259

1203-
def docmodule(self, object, name=None, mod=None):
1260+
def docmodule(self, object, name=None, mod=None, *ignored):
12041261
"""Produce text documentation for a given module object."""
12051262
name = object.__name__ # ignore the passed-in name
12061263
synop, desc = splitdoc(getdoc(object))
@@ -1384,7 +1441,7 @@ def spill(msg, attrs, predicate):
13841441
push(self.docdata(value, name, mod))
13851442
else:
13861443
push(self.document(value,
1387-
name, mod, object))
1444+
name, mod, object, homecls))
13881445
return attrs
13891446

13901447
def spilldescriptors(msg, attrs, predicate):
@@ -1459,23 +1516,43 @@ def formatvalue(self, object):
14591516
"""Format an argument default value as text."""
14601517
return '=' + self.repr(object)
14611518

1462-
def docroutine(self, object, name=None, mod=None, cl=None):
1519+
def docroutine(self, object, name=None, mod=None, cl=None, homecls=None):
14631520
"""Produce text documentation for a function or method object."""
14641521
realname = object.__name__
14651522
name = name or realname
1523+
if homecls is None:
1524+
homecls = cl
14661525
note = ''
1467-
skipdocs = 0
1526+
skipdocs = False
1527+
imfunc = None
14681528
if _is_bound_method(object):
1469-
imclass = object.__self__.__class__
1470-
if cl:
1471-
if imclass is not cl:
1472-
note = ' from ' + classname(imclass, mod)
1529+
imself = object.__self__
1530+
if imself is cl:
1531+
imfunc = getattr(object, '__func__', None)
1532+
elif inspect.isclass(imself):
1533+
note = ' class method of %s' % classname(imself, mod)
14731534
else:
1474-
if object.__self__ is not None:
1475-
note = ' method of %s instance' % classname(
1476-
object.__self__.__class__, mod)
1477-
else:
1478-
note = ' unbound %s method' % classname(imclass,mod)
1535+
note = ' method of %s instance' % classname(
1536+
imself.__class__, mod)
1537+
elif (inspect.ismethoddescriptor(object) or
1538+
inspect.ismethodwrapper(object)):
1539+
try:
1540+
objclass = object.__objclass__
1541+
except AttributeError:
1542+
pass
1543+
else:
1544+
if cl is None:
1545+
note = ' unbound %s method' % classname(objclass, mod)
1546+
elif objclass is not homecls:
1547+
note = ' from ' + classname(objclass, mod)
1548+
else:
1549+
imfunc = object
1550+
if inspect.isfunction(imfunc) and homecls is not None and (
1551+
imfunc.__module__ != homecls.__module__ or
1552+
imfunc.__qualname__ != homecls.__qualname__ + '.' + realname):
1553+
pname = parentname(imfunc, mod)
1554+
if pname:
1555+
note = ' from %s' % pname
14791556

14801557
if (inspect.iscoroutinefunction(object) or
14811558
inspect.isasyncgenfunction(object)):
@@ -1486,8 +1563,11 @@ def docroutine(self, object, name=None, mod=None, cl=None):
14861563
if name == realname:
14871564
title = self.bold(realname)
14881565
else:
1489-
if cl and inspect.getattr_static(cl, realname, []) is object:
1490-
skipdocs = 1
1566+
if (cl is not None and
1567+
inspect.getattr_static(cl, realname, []) is object):
1568+
skipdocs = True
1569+
if note.startswith(' from '):
1570+
note = ''
14911571
title = self.bold(name) + ' = ' + realname
14921572
argspec = None
14931573

@@ -1514,7 +1594,7 @@ def docroutine(self, object, name=None, mod=None, cl=None):
15141594
doc = getdoc(object) or ''
15151595
return decl + '\n' + (doc and self.indent(doc).rstrip() + '\n')
15161596

1517-
def docdata(self, object, name=None, mod=None, cl=None):
1597+
def docdata(self, object, name=None, mod=None, cl=None, *ignored):
15181598
"""Produce text documentation for a data descriptor."""
15191599
results = []
15201600
push = results.append
@@ -1530,7 +1610,8 @@ def docdata(self, object, name=None, mod=None, cl=None):
15301610

15311611
docproperty = docdata
15321612

1533-
def docother(self, object, name=None, mod=None, parent=None, maxlen=None, doc=None):
1613+
def docother(self, object, name=None, mod=None, parent=None, *ignored,
1614+
maxlen=None, doc=None):
15341615
"""Produce text documentation for a data object."""
15351616
repr = self.repr(object)
15361617
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
@@ -4752,22 +4752,22 @@ class Color(enum.Enum)
47524752
| The value of the Enum member.
47534753
|
47544754
| ----------------------------------------------------------------------
4755-
| Methods inherited from enum.EnumType:
4755+
| Static methods inherited from enum.EnumType:
47564756
|
4757-
| __contains__(value) from enum.EnumType
4757+
| __contains__(value)
47584758
| Return True if `value` is in `cls`.
47594759
|
47604760
| `value` is in `cls` if:
47614761
| 1) `value` is a member of `cls`, or
47624762
| 2) `value` is the value of one of the `cls`'s members.
47634763
|
4764-
| __getitem__(name) from enum.EnumType
4764+
| __getitem__(name)
47654765
| Return the member matching `name`.
47664766
|
4767-
| __iter__() from enum.EnumType
4767+
| __iter__()
47684768
| Return members in definition order.
47694769
|
4770-
| __len__() from enum.EnumType
4770+
| __len__()
47714771
| Return the number of members (no aliases)
47724772
|
47734773
| ----------------------------------------------------------------------

0 commit comments

Comments
 (0)