Skip to content

Commit 2939ad0

Browse files
gh-97959: Fix rendering of routines in pydoc (GH-113941)
* 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.
1 parent b104360 commit 2939ad0

File tree

5 files changed

+333
-72
lines changed

5 files changed

+333
-72
lines changed

Lib/pydoc.py

+115-34
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,19 @@ def classname(object, modname):
225225
name = object.__module__ + '.' + name
226226
return name
227227

228+
def parentname(object, modname):
229+
"""Get a name of the enclosing class (qualified it with a module name
230+
if necessary) or module."""
231+
if '.' in object.__qualname__:
232+
name = object.__qualname__.rpartition('.')[0]
233+
if object.__module__ != modname:
234+
return object.__module__ + '.' + name
235+
else:
236+
return name
237+
else:
238+
if object.__module__ != modname:
239+
return object.__module__
240+
228241
def isdata(object):
229242
"""Check if an object is of a type that probably means it's data."""
230243
return not (inspect.ismodule(object) or inspect.isclass(object) or
@@ -319,13 +332,15 @@ def visiblename(name, all=None, obj=None):
319332
return not name.startswith('_')
320333

321334
def classify_class_attrs(object):
322-
"""Wrap inspect.classify_class_attrs, with fixup for data descriptors."""
335+
"""Wrap inspect.classify_class_attrs, with fixup for data descriptors and bound methods."""
323336
results = []
324337
for (name, kind, cls, value) in inspect.classify_class_attrs(object):
325338
if inspect.isdatadescriptor(value):
326339
kind = 'data descriptor'
327340
if isinstance(value, property) and value.fset is None:
328341
kind = 'readonly property'
342+
elif kind == 'method' and _is_bound_method(value):
343+
kind = 'static method'
329344
results.append((name, kind, cls, value))
330345
return results
331346

@@ -681,6 +696,25 @@ def classlink(self, object, modname):
681696
module.__name__, name, classname(object, modname))
682697
return classname(object, modname)
683698

699+
def parentlink(self, object, modname):
700+
"""Make a link for the enclosing class or module."""
701+
link = None
702+
name, module = object.__name__, sys.modules.get(object.__module__)
703+
if hasattr(module, name) and getattr(module, name) is object:
704+
if '.' in object.__qualname__:
705+
name = object.__qualname__.rpartition('.')[0]
706+
if object.__module__ != modname:
707+
link = '%s.html#%s' % (module.__name__, name)
708+
else:
709+
link = '#%s' % name
710+
else:
711+
if object.__module__ != modname:
712+
link = '%s.html' % module.__name__
713+
if link:
714+
return '<a href="%s">%s</a>' % (link, parentname(object, modname))
715+
else:
716+
return parentname(object, modname)
717+
684718
def modulelink(self, object):
685719
"""Make a link for a module."""
686720
return '<a href="%s.html">%s</a>' % (object.__name__, object.__name__)
@@ -925,7 +959,7 @@ def spill(msg, attrs, predicate):
925959
push(self.docdata(value, name, mod))
926960
else:
927961
push(self.document(value, name, mod,
928-
funcs, classes, mdict, object))
962+
funcs, classes, mdict, object, homecls))
929963
push('\n')
930964
return attrs
931965

@@ -1043,24 +1077,44 @@ def formatvalue(self, object):
10431077
return self.grey('=' + self.repr(object))
10441078

10451079
def docroutine(self, object, name=None, mod=None,
1046-
funcs={}, classes={}, methods={}, cl=None):
1080+
funcs={}, classes={}, methods={}, cl=None, homecls=None):
10471081
"""Produce HTML documentation for a function or method object."""
10481082
realname = object.__name__
10491083
name = name or realname
1050-
anchor = (cl and cl.__name__ or '') + '-' + name
1084+
if homecls is None:
1085+
homecls = cl
1086+
anchor = ('' if cl is None else cl.__name__) + '-' + name
10511087
note = ''
1052-
skipdocs = 0
1088+
skipdocs = False
1089+
imfunc = None
10531090
if _is_bound_method(object):
1054-
imclass = object.__self__.__class__
1055-
if cl:
1056-
if imclass is not cl:
1057-
note = ' from ' + self.classlink(imclass, mod)
1091+
imself = object.__self__
1092+
if imself is cl:
1093+
imfunc = getattr(object, '__func__', None)
1094+
elif inspect.isclass(imself):
1095+
note = ' class method of %s' % self.classlink(imself, mod)
10581096
else:
1059-
if object.__self__ is not None:
1060-
note = ' method of %s instance' % self.classlink(
1061-
object.__self__.__class__, mod)
1062-
else:
1063-
note = ' unbound %s method' % self.classlink(imclass,mod)
1097+
note = ' method of %s instance' % self.classlink(
1098+
imself.__class__, mod)
1099+
elif (inspect.ismethoddescriptor(object) or
1100+
inspect.ismethodwrapper(object)):
1101+
try:
1102+
objclass = object.__objclass__
1103+
except AttributeError:
1104+
pass
1105+
else:
1106+
if cl is None:
1107+
note = ' unbound %s method' % self.classlink(objclass, mod)
1108+
elif objclass is not homecls:
1109+
note = ' from ' + self.classlink(objclass, mod)
1110+
else:
1111+
imfunc = object
1112+
if inspect.isfunction(imfunc) and homecls is not None and (
1113+
imfunc.__module__ != homecls.__module__ or
1114+
imfunc.__qualname__ != homecls.__qualname__ + '.' + realname):
1115+
pname = self.parentlink(imfunc, mod)
1116+
if pname:
1117+
note = ' from %s' % pname
10641118

10651119
if (inspect.iscoroutinefunction(object) or
10661120
inspect.isasyncgenfunction(object)):
@@ -1071,10 +1125,13 @@ def docroutine(self, object, name=None, mod=None,
10711125
if name == realname:
10721126
title = '<a name="%s"><strong>%s</strong></a>' % (anchor, realname)
10731127
else:
1074-
if cl and inspect.getattr_static(cl, realname, []) is object:
1128+
if (cl is not None and
1129+
inspect.getattr_static(cl, realname, []) is object):
10751130
reallink = '<a href="#%s">%s</a>' % (
10761131
cl.__name__ + '-' + realname, realname)
1077-
skipdocs = 1
1132+
skipdocs = True
1133+
if note.startswith(' from '):
1134+
note = ''
10781135
else:
10791136
reallink = realname
10801137
title = '<a name="%s"><strong>%s</strong></a> = %s' % (
@@ -1102,7 +1159,7 @@ def docroutine(self, object, name=None, mod=None,
11021159
doc = doc and '<dd><span class="code">%s</span></dd>' % doc
11031160
return '<dl><dt>%s</dt>%s</dl>\n' % (decl, doc)
11041161

1105-
def docdata(self, object, name=None, mod=None, cl=None):
1162+
def docdata(self, object, name=None, mod=None, cl=None, *ignored):
11061163
"""Produce html documentation for a data descriptor."""
11071164
results = []
11081165
push = results.append
@@ -1213,7 +1270,7 @@ def formattree(self, tree, modname, parent=None, prefix=''):
12131270
entry, modname, c, prefix + ' ')
12141271
return result
12151272

1216-
def docmodule(self, object, name=None, mod=None):
1273+
def docmodule(self, object, name=None, mod=None, *ignored):
12171274
"""Produce text documentation for a given module object."""
12181275
name = object.__name__ # ignore the passed-in name
12191276
synop, desc = splitdoc(getdoc(object))
@@ -1392,7 +1449,7 @@ def spill(msg, attrs, predicate):
13921449
push(self.docdata(value, name, mod))
13931450
else:
13941451
push(self.document(value,
1395-
name, mod, object))
1452+
name, mod, object, homecls))
13961453
return attrs
13971454

13981455
def spilldescriptors(msg, attrs, predicate):
@@ -1467,23 +1524,43 @@ def formatvalue(self, object):
14671524
"""Format an argument default value as text."""
14681525
return '=' + self.repr(object)
14691526

1470-
def docroutine(self, object, name=None, mod=None, cl=None):
1527+
def docroutine(self, object, name=None, mod=None, cl=None, homecls=None):
14711528
"""Produce text documentation for a function or method object."""
14721529
realname = object.__name__
14731530
name = name or realname
1531+
if homecls is None:
1532+
homecls = cl
14741533
note = ''
1475-
skipdocs = 0
1534+
skipdocs = False
1535+
imfunc = None
14761536
if _is_bound_method(object):
1477-
imclass = object.__self__.__class__
1478-
if cl:
1479-
if imclass is not cl:
1480-
note = ' from ' + classname(imclass, mod)
1537+
imself = object.__self__
1538+
if imself is cl:
1539+
imfunc = getattr(object, '__func__', None)
1540+
elif inspect.isclass(imself):
1541+
note = ' class method of %s' % classname(imself, mod)
14811542
else:
1482-
if object.__self__ is not None:
1483-
note = ' method of %s instance' % classname(
1484-
object.__self__.__class__, mod)
1485-
else:
1486-
note = ' unbound %s method' % classname(imclass,mod)
1543+
note = ' method of %s instance' % classname(
1544+
imself.__class__, mod)
1545+
elif (inspect.ismethoddescriptor(object) or
1546+
inspect.ismethodwrapper(object)):
1547+
try:
1548+
objclass = object.__objclass__
1549+
except AttributeError:
1550+
pass
1551+
else:
1552+
if cl is None:
1553+
note = ' unbound %s method' % classname(objclass, mod)
1554+
elif objclass is not homecls:
1555+
note = ' from ' + classname(objclass, mod)
1556+
else:
1557+
imfunc = object
1558+
if inspect.isfunction(imfunc) and homecls is not None and (
1559+
imfunc.__module__ != homecls.__module__ or
1560+
imfunc.__qualname__ != homecls.__qualname__ + '.' + realname):
1561+
pname = parentname(imfunc, mod)
1562+
if pname:
1563+
note = ' from %s' % pname
14871564

14881565
if (inspect.iscoroutinefunction(object) or
14891566
inspect.isasyncgenfunction(object)):
@@ -1494,8 +1571,11 @@ def docroutine(self, object, name=None, mod=None, cl=None):
14941571
if name == realname:
14951572
title = self.bold(realname)
14961573
else:
1497-
if cl and inspect.getattr_static(cl, realname, []) is object:
1498-
skipdocs = 1
1574+
if (cl is not None and
1575+
inspect.getattr_static(cl, realname, []) is object):
1576+
skipdocs = True
1577+
if note.startswith(' from '):
1578+
note = ''
14991579
title = self.bold(name) + ' = ' + realname
15001580
argspec = None
15011581

@@ -1517,7 +1597,7 @@ def docroutine(self, object, name=None, mod=None, cl=None):
15171597
doc = getdoc(object) or ''
15181598
return decl + '\n' + (doc and self.indent(doc).rstrip() + '\n')
15191599

1520-
def docdata(self, object, name=None, mod=None, cl=None):
1600+
def docdata(self, object, name=None, mod=None, cl=None, *ignored):
15211601
"""Produce text documentation for a data descriptor."""
15221602
results = []
15231603
push = results.append
@@ -1533,7 +1613,8 @@ def docdata(self, object, name=None, mod=None, cl=None):
15331613

15341614
docproperty = docdata
15351615

1536-
def docother(self, object, name=None, mod=None, parent=None, maxlen=None, doc=None):
1616+
def docother(self, object, name=None, mod=None, parent=None, *ignored,
1617+
maxlen=None, doc=None):
15371618
"""Produce text documentation for a data object."""
15381619
repr = self.repr(object)
15391620
if maxlen:

Lib/test/pydocfodder.py

+47-1
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

+5-5
Original file line numberDiff line numberDiff line change
@@ -4851,22 +4851,22 @@ class Color(enum.Enum)
48514851
| The value of the Enum member.
48524852
|
48534853
| ----------------------------------------------------------------------
4854-
| Methods inherited from enum.EnumType:
4854+
| Static methods inherited from enum.EnumType:
48554855
|
4856-
| __contains__(value) from enum.EnumType
4856+
| __contains__(value)
48574857
| Return True if `value` is in `cls`.
48584858
|
48594859
| `value` is in `cls` if:
48604860
| 1) `value` is a member of `cls`, or
48614861
| 2) `value` is the value of one of the `cls`'s members.
48624862
|
4863-
| __getitem__(name) from enum.EnumType
4863+
| __getitem__(name)
48644864
| Return the member matching `name`.
48654865
|
4866-
| __iter__() from enum.EnumType
4866+
| __iter__()
48674867
| Return members in definition order.
48684868
|
4869-
| __len__() from enum.EnumType
4869+
| __len__()
48704870
| Return the number of members (no aliases)
48714871
|
48724872
| ----------------------------------------------------------------------

0 commit comments

Comments
 (0)