Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.11] [3.12] gh-97959: Fix rendering of routines in pydoc (GH-113941) (GH-115296) #115302

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
149 changes: 115 additions & 34 deletions Lib/pydoc.py
Expand Up @@ -204,6 +204,19 @@ def classname(object, modname):
name = object.__module__ + '.' + name
return name

def parentname(object, modname):
"""Get a name of the enclosing class (qualified it with a module name
if necessary) or module."""
if '.' in object.__qualname__:
name = object.__qualname__.rpartition('.')[0]
if object.__module__ != modname:
return object.__module__ + '.' + name
else:
return name
else:
if object.__module__ != modname:
return object.__module__

def isdata(object):
"""Check if an object is of a type that probably means it's data."""
return not (inspect.ismodule(object) or inspect.isclass(object) or
Expand Down Expand Up @@ -298,13 +311,15 @@ def visiblename(name, all=None, obj=None):
return not name.startswith('_')

def classify_class_attrs(object):
"""Wrap inspect.classify_class_attrs, with fixup for data descriptors."""
"""Wrap inspect.classify_class_attrs, with fixup for data descriptors and bound methods."""
results = []
for (name, kind, cls, value) in inspect.classify_class_attrs(object):
if inspect.isdatadescriptor(value):
kind = 'data descriptor'
if isinstance(value, property) and value.fset is None:
kind = 'readonly property'
elif kind == 'method' and _is_bound_method(value):
kind = 'static method'
results.append((name, kind, cls, value))
return results

Expand Down Expand Up @@ -653,6 +668,25 @@ def classlink(self, object, modname):
module.__name__, name, classname(object, modname))
return classname(object, modname)

def parentlink(self, object, modname):
"""Make a link for the enclosing class or module."""
link = None
name, module = object.__name__, sys.modules.get(object.__module__)
if hasattr(module, name) and getattr(module, name) is object:
if '.' in object.__qualname__:
name = object.__qualname__.rpartition('.')[0]
if object.__module__ != modname:
link = '%s.html#%s' % (module.__name__, name)
else:
link = '#%s' % name
else:
if object.__module__ != modname:
link = '%s.html' % module.__name__
if link:
return '<a href="%s">%s</a>' % (link, parentname(object, modname))
else:
return parentname(object, modname)

def modulelink(self, object):
"""Make a link for a module."""
return '<a href="%s.html">%s</a>' % (object.__name__, object.__name__)
Expand Down Expand Up @@ -899,7 +933,7 @@ def spill(msg, attrs, predicate):
push(self.docdata(value, name, mod))
else:
push(self.document(value, name, mod,
funcs, classes, mdict, object))
funcs, classes, mdict, object, homecls))
push('\n')
return attrs

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

def docroutine(self, object, name=None, mod=None,
funcs={}, classes={}, methods={}, cl=None):
funcs={}, classes={}, methods={}, cl=None, homecls=None):
"""Produce HTML documentation for a function or method object."""
realname = object.__name__
name = name or realname
anchor = (cl and cl.__name__ or '') + '-' + name
if homecls is None:
homecls = cl
anchor = ('' if cl is None else cl.__name__) + '-' + name
note = ''
skipdocs = 0
skipdocs = False
imfunc = None
if _is_bound_method(object):
imclass = object.__self__.__class__
if cl:
if imclass is not cl:
note = ' from ' + self.classlink(imclass, mod)
imself = object.__self__
if imself is cl:
imfunc = getattr(object, '__func__', None)
elif inspect.isclass(imself):
note = ' class method of %s' % self.classlink(imself, mod)
else:
if object.__self__ is not None:
note = ' method of %s instance' % self.classlink(
object.__self__.__class__, mod)
else:
note = ' unbound %s method' % self.classlink(imclass,mod)
note = ' method of %s instance' % self.classlink(
imself.__class__, mod)
elif (inspect.ismethoddescriptor(object) or
inspect.ismethodwrapper(object)):
try:
objclass = object.__objclass__
except AttributeError:
pass
else:
if cl is None:
note = ' unbound %s method' % self.classlink(objclass, mod)
elif objclass is not homecls:
note = ' from ' + self.classlink(objclass, mod)
else:
imfunc = object
if inspect.isfunction(imfunc) and homecls is not None and (
imfunc.__module__ != homecls.__module__ or
imfunc.__qualname__ != homecls.__qualname__ + '.' + realname):
pname = self.parentlink(imfunc, mod)
if pname:
note = ' from %s' % pname

if (inspect.iscoroutinefunction(object) or
inspect.isasyncgenfunction(object)):
Expand All @@ -1050,10 +1104,13 @@ def docroutine(self, object, name=None, mod=None,
if name == realname:
title = '<a name="%s"><strong>%s</strong></a>' % (anchor, realname)
else:
if cl and inspect.getattr_static(cl, realname, []) is object:
if (cl is not None and
inspect.getattr_static(cl, realname, []) is object):
reallink = '<a href="#%s">%s</a>' % (
cl.__name__ + '-' + realname, realname)
skipdocs = 1
skipdocs = True
if note.startswith(' from '):
note = ''
else:
reallink = realname
title = '<a name="%s"><strong>%s</strong></a> = %s' % (
Expand Down Expand Up @@ -1086,7 +1143,7 @@ def docroutine(self, object, name=None, mod=None,
doc = doc and '<dd><span class="code">%s</span></dd>' % doc
return '<dl><dt>%s</dt>%s</dl>\n' % (decl, doc)

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

def docmodule(self, object, name=None, mod=None):
def docmodule(self, object, name=None, mod=None, *ignored):
"""Produce text documentation for a given module object."""
name = object.__name__ # ignore the passed-in name
synop, desc = splitdoc(getdoc(object))
Expand Down Expand Up @@ -1382,7 +1439,7 @@ def spill(msg, attrs, predicate):
push(self.docdata(value, name, mod))
else:
push(self.document(value,
name, mod, object))
name, mod, object, homecls))
return attrs

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

def docroutine(self, object, name=None, mod=None, cl=None):
def docroutine(self, object, name=None, mod=None, cl=None, homecls=None):
"""Produce text documentation for a function or method object."""
realname = object.__name__
name = name or realname
if homecls is None:
homecls = cl
note = ''
skipdocs = 0
skipdocs = False
imfunc = None
if _is_bound_method(object):
imclass = object.__self__.__class__
if cl:
if imclass is not cl:
note = ' from ' + classname(imclass, mod)
imself = object.__self__
if imself is cl:
imfunc = getattr(object, '__func__', None)
elif inspect.isclass(imself):
note = ' class method of %s' % classname(imself, mod)
else:
if object.__self__ is not None:
note = ' method of %s instance' % classname(
object.__self__.__class__, mod)
else:
note = ' unbound %s method' % classname(imclass,mod)
note = ' method of %s instance' % classname(
imself.__class__, mod)
elif (inspect.ismethoddescriptor(object) or
inspect.ismethodwrapper(object)):
try:
objclass = object.__objclass__
except AttributeError:
pass
else:
if cl is None:
note = ' unbound %s method' % classname(objclass, mod)
elif objclass is not homecls:
note = ' from ' + classname(objclass, mod)
else:
imfunc = object
if inspect.isfunction(imfunc) and homecls is not None and (
imfunc.__module__ != homecls.__module__ or
imfunc.__qualname__ != homecls.__qualname__ + '.' + realname):
pname = parentname(imfunc, mod)
if pname:
note = ' from %s' % pname

if (inspect.iscoroutinefunction(object) or
inspect.isasyncgenfunction(object)):
Expand All @@ -1484,8 +1561,11 @@ def docroutine(self, object, name=None, mod=None, cl=None):
if name == realname:
title = self.bold(realname)
else:
if cl and inspect.getattr_static(cl, realname, []) is object:
skipdocs = 1
if (cl is not None and
inspect.getattr_static(cl, realname, []) is object):
skipdocs = True
if note.startswith(' from '):
note = ''
title = self.bold(name) + ' = ' + realname
argspec = None

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

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

docproperty = docdata

def docother(self, object, name=None, mod=None, parent=None, maxlen=None, doc=None):
def docother(self, object, name=None, mod=None, parent=None, *ignored,
maxlen=None, doc=None):
"""Produce text documentation for a data object."""
repr = self.repr(object)
if maxlen:
Expand Down
48 changes: 47 additions & 1 deletion Lib/test/pydocfodder.py
Expand Up @@ -2,6 +2,12 @@

import types

def global_func(x, y):
"""Module global function"""

def global_func2(x, y):
"""Module global function 2"""

class A:
"A class."

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

def A_staticmethod():
def A_staticmethod(x, y):
"A static method defined in A."
A_staticmethod = staticmethod(A_staticmethod)

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

@classmethod
def B_classmethod(cls, x):
"A class method defined in B."

global_func = global_func # same name
global_func_alias = global_func
global_func2_alias = global_func2
B_classmethod_alias = B_classmethod
A_classmethod_ref = A.A_classmethod
A_staticmethod = A.A_staticmethod # same name
A_staticmethod_alias = A.A_staticmethod
A_method_ref = A().A_method
A_method_alias = A.A_method
B_method_alias = B_method
__repr__ = object.__repr__ # same name
object_repr = object.__repr__
get = {}.get # same name
dict_get = {}.get

B.B_classmethod_ref = B.B_classmethod


class C(A):
"A class, derived from A."

Expand Down Expand Up @@ -136,3 +164,21 @@ def __call__(self, inst):

submodule = types.ModuleType(__name__ + '.submodule',
"""A submodule, which should appear in its parent's summary""")

global_func_alias = global_func
A_classmethod = A.A_classmethod # same name
A_classmethod2 = A.A_classmethod
A_classmethod3 = B.A_classmethod
A_staticmethod = A.A_staticmethod # same name
A_staticmethod_alias = A.A_staticmethod
A_staticmethod_ref = A().A_staticmethod
A_staticmethod_ref2 = B().A_staticmethod
A_method = A().A_method # same name
A_method2 = A().A_method
A_method3 = B().A_method
B_method = B.B_method # same name
B_method2 = B.B_method
count = list.count # same name
list_count = list.count
get = {}.get # same name
dict_get = {}.get
10 changes: 5 additions & 5 deletions Lib/test/test_enum.py
Expand Up @@ -4443,22 +4443,22 @@ class Color(enum.Enum)
| The value of the Enum member.
|\x20\x20
| ----------------------------------------------------------------------
| Methods inherited from enum.EnumType:
| Static methods inherited from enum.EnumType:
|\x20\x20
| __contains__(member) from enum.EnumType
| __contains__(member)
| Return True if member is a member of this enum
| raises TypeError if member is not an enum member
|\x20\x20\x20\x20\x20\x20
| note: in 3.12 TypeError will no longer be raised, and True will also be
| returned if member is the value of a member in this enum
|\x20\x20
| __getitem__(name) from enum.EnumType
| __getitem__(name)
| Return the member matching `name`.
|\x20\x20
| __iter__() from enum.EnumType
| __iter__()
| Return members in definition order.
|\x20\x20
| __len__() from enum.EnumType
| __len__()
| Return the number of members (no aliases)
|\x20\x20
| ----------------------------------------------------------------------
Expand Down