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.
This commit is contained in:
Serhiy Storchaka 2024-02-11 15:19:44 +02:00 committed by GitHub
parent b104360788
commit 2939ad02be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 333 additions and 72 deletions

View file

@ -225,6 +225,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
@ -319,13 +332,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
@ -681,6 +696,25 @@ class HTMLDoc(Doc):
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__)
@ -925,7 +959,7 @@ class HTMLDoc(Doc):
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
@ -1043,24 +1077,44 @@ class HTMLDoc(Doc):
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)):
@ -1071,10 +1125,13 @@ class HTMLDoc(Doc):
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' % (
@ -1102,7 +1159,7 @@ class HTMLDoc(Doc):
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
@ -1213,7 +1270,7 @@ class TextDoc(Doc):
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))
@ -1392,7 +1449,7 @@ location listed above.
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):
@ -1467,23 +1524,43 @@ location listed above.
"""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)):
@ -1494,8 +1571,11 @@ location listed above.
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
@ -1517,7 +1597,7 @@ location listed above.
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
@ -1533,7 +1613,8 @@ location listed above.
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:

View file

@ -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."
@ -26,7 +32,7 @@ class A:
"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)
@ -61,6 +67,28 @@ class B(A):
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."
@ -136,3 +164,21 @@ class FunkyProperties(object):
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

View file

@ -4851,22 +4851,22 @@ class Color(enum.Enum)
| The value of the Enum member.
|
| ----------------------------------------------------------------------
| Methods inherited from enum.EnumType:
| Static methods inherited from enum.EnumType:
|
| __contains__(value) from enum.EnumType
| __contains__(value)
| Return True if `value` is in `cls`.
|
| `value` is in `cls` if:
| 1) `value` is a member of `cls`, or
| 2) `value` is the value of one of the `cls`'s members.
|
| __getitem__(name) from enum.EnumType
| __getitem__(name)
| Return the member matching `name`.
|
| __iter__() from enum.EnumType
| __iter__()
| Return members in definition order.
|
| __len__() from enum.EnumType
| __len__()
| Return the number of members (no aliases)
|
| ----------------------------------------------------------------------

View file

@ -35,6 +35,7 @@ from test.support import (reap_children, captured_output, captured_stdout,
requires_docstrings, MISSING_C_DOCSTRINGS)
from test.support.os_helper import (TESTFN, rmtree, unlink)
from test import pydoc_mod
from test import pydocfodder
class nonascii:
@ -102,7 +103,7 @@ CLASSES
| ----------------------------------------------------------------------
| Class methods defined here:
|
| __class_getitem__(item) from builtins.type
| __class_getitem__(item)
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
@ -166,7 +167,7 @@ class A(builtins.object)
Methods defined here:
__init__()
Wow, I have no function!
----------------------------------------------------------------------
Data descriptors defined here:
__dict__
dictionary for instance variables
@ -179,6 +180,7 @@ class B(builtins.object)
dictionary for instance variables
__weakref__
list of weak references to the object
----------------------------------------------------------------------
Data and other attributes defined here:
NO_MEANING = 'eggs'
__annotations__ = {'NO_MEANING': <class 'str'>}
@ -191,8 +193,10 @@ class C(builtins.object)
is_it_true(self)
Return self.get_answer()
say_no(self)
----------------------------------------------------------------------
Class methods defined here:
__class_getitem__(item) from builtins.type
__class_getitem__(item)
----------------------------------------------------------------------
Data descriptors defined here:
__dict__
dictionary for instance variables
@ -330,6 +334,10 @@ def get_pydoc_html(module):
loc = "<br><a href=\"" + loc + "\">Module Docs</a>"
return output.strip(), loc
def clean_text(doc):
# clean up the extra text formatting that pydoc performs
return re.sub('\b.', '', doc)
def get_pydoc_link(module):
"Returns a documentation web link of a module"
abspath = os.path.abspath
@ -347,10 +355,7 @@ def get_pydoc_text(module):
loc = "\nMODULE DOCS\n " + loc + "\n"
output = doc.docmodule(module)
# clean up the extra text formatting that pydoc performs
patt = re.compile('\b.')
output = patt.sub('', output)
output = clean_text(output)
return output.strip(), loc
def get_html_title(text):
@ -367,6 +372,7 @@ def html2text(html):
Tailored for pydoc tests only.
"""
html = html.replace("<dd>", "\n")
html = html.replace("<hr>", "-"*70)
html = re.sub("<.*?>", "", html)
html = pydoc.replace(html, "&nbsp;", " ", "&gt;", ">", "&lt;", "<")
return html
@ -798,8 +804,7 @@ class PydocDocTest(unittest.TestCase):
b_size = A.a_size
doc = pydoc.render_doc(B)
# clean up the extra text formatting that pydoc performs
doc = re.sub('\b.', '', doc)
doc = clean_text(doc)
self.assertEqual(doc, '''\
Python Library Documentation: class B in module %s
@ -887,8 +892,7 @@ class B(A)
...
doc = pydoc.render_doc(A)
# clean up the extra text formatting that pydoc performs
doc = re.sub('\b.', '', doc)
doc = clean_text(doc)
self.assertEqual(doc, '''Python Library Documentation: class A in module %s
class A(builtins.object)
@ -925,8 +929,7 @@ class A(builtins.object)
...
doc = pydoc.render_doc(func)
# clean up the extra text formatting that pydoc performs
doc = re.sub('\b.', '', doc)
doc = clean_text(doc)
self.assertEqual(doc, '''Python Library Documentation: function func in module %s
func(
@ -942,8 +945,7 @@ func(
...
doc = pydoc.render_doc(function_with_really_long_name_so_annotations_can_be_rather_small)
# clean up the extra text formatting that pydoc performs
doc = re.sub('\b.', '', doc)
doc = clean_text(doc)
self.assertEqual(doc, '''Python Library Documentation: function function_with_really_long_name_so_annotations_can_be_rather_small in module %s
function_with_really_long_name_so_annotations_can_be_rather_small(
@ -957,8 +959,7 @@ function_with_really_long_name_so_annotations_can_be_rather_small(
second_very_long_parameter_name: ...
doc = pydoc.render_doc(does_not_have_name)
# clean up the extra text formatting that pydoc performs
doc = re.sub('\b.', '', doc)
doc = clean_text(doc)
self.assertEqual(doc, '''Python Library Documentation: function <lambda> in module %s
<lambda> lambda very_long_parameter_name_that_should_not_fit_into_a_single_line, second_very_long_parameter_name
@ -1244,7 +1245,7 @@ class TestDescriptions(unittest.TestCase):
@requires_docstrings
def test_unbound_builtin_method(self):
self.assertEqual(self._get_summary_line(_pickle.Pickler.dump),
"dump(self, obj, /)")
"dump(self, obj, /) unbound _pickle.Pickler method")
# these no longer include "self"
def test_bound_python_method(self):
@ -1296,7 +1297,7 @@ class TestDescriptions(unittest.TestCase):
def test_unbound_builtin_method_noargs(self):
self.assertEqual(self._get_summary_line(str.lower),
"lower(self, /)")
"lower(self, /) unbound builtins.str method")
def test_bound_builtin_method_noargs(self):
self.assertEqual(self._get_summary_line(''.lower),
@ -1304,7 +1305,7 @@ class TestDescriptions(unittest.TestCase):
def test_unbound_builtin_method_o(self):
self.assertEqual(self._get_summary_line(set.add),
"add(self, object, /)")
"add(self, object, /) unbound builtins.set method")
def test_bound_builtin_method_o(self):
self.assertEqual(self._get_summary_line(set().add),
@ -1312,7 +1313,7 @@ class TestDescriptions(unittest.TestCase):
def test_unbound_builtin_method_coexist_o(self):
self.assertEqual(self._get_summary_line(set.__contains__),
"__contains__(self, object, /)")
"__contains__(self, object, /) unbound builtins.set method")
def test_bound_builtin_method_coexist_o(self):
self.assertEqual(self._get_summary_line(set().__contains__),
@ -1320,19 +1321,19 @@ class TestDescriptions(unittest.TestCase):
def test_unbound_builtin_classmethod_noargs(self):
self.assertEqual(self._get_summary_line(datetime.datetime.__dict__['utcnow']),
"utcnow(type, /)")
"utcnow(type, /) unbound datetime.datetime method")
def test_bound_builtin_classmethod_noargs(self):
self.assertEqual(self._get_summary_line(datetime.datetime.utcnow),
"utcnow() method of builtins.type instance")
"utcnow() class method of datetime.datetime")
def test_unbound_builtin_classmethod_o(self):
self.assertEqual(self._get_summary_line(dict.__dict__['__class_getitem__']),
"__class_getitem__(type, object, /)")
"__class_getitem__(type, object, /) unbound builtins.dict method")
def test_bound_builtin_classmethod_o(self):
self.assertEqual(self._get_summary_line(dict.__class_getitem__),
"__class_getitem__(object, /) method of builtins.type instance")
"__class_getitem__(object, /) class method of builtins.dict")
@support.cpython_only
@requires_docstrings
@ -1356,11 +1357,13 @@ class TestDescriptions(unittest.TestCase):
@requires_docstrings
def test_unbound_builtin_method_unrepresentable_default(self):
self.assertEqual(self._get_summary_line(dict.pop),
"pop(self, key, default=<unrepresentable>, /)")
"pop(self, key, default=<unrepresentable>, /) "
"unbound builtins.dict method")
import _testcapi
cls = _testcapi.DocStringUnrepresentableSignatureTest
self.assertEqual(self._get_summary_line(cls.meth),
"meth(self, /, a, b=<x>)")
"meth(self, /, a, b=<x>) unbound "
"_testcapi.DocStringUnrepresentableSignatureTest method")
@support.cpython_only
@requires_docstrings
@ -1381,7 +1384,8 @@ class TestDescriptions(unittest.TestCase):
cls = _testcapi.DocStringUnrepresentableSignatureTest
descr = cls.__dict__['classmeth']
self.assertEqual(self._get_summary_line(descr),
"classmeth(type, /, a, b=<x>)")
"classmeth(type, /, a, b=<x>) unbound "
"_testcapi.DocStringUnrepresentableSignatureTest method")
@support.cpython_only
@requires_docstrings
@ -1389,7 +1393,8 @@ class TestDescriptions(unittest.TestCase):
import _testcapi
cls = _testcapi.DocStringUnrepresentableSignatureTest
self.assertEqual(self._get_summary_line(cls.classmeth),
"classmeth(a, b=<x>) method of builtins.type instance")
"classmeth(a, b=<x>) class method of "
"_testcapi.DocStringUnrepresentableSignatureTest")
def test_overridden_text_signature(self):
class C:
@ -1423,7 +1428,7 @@ class TestDescriptions(unittest.TestCase):
"meth" + bound + " method of test.test_pydoc.C instance")
C.cmeth.__func__.__text_signature__ = text_signature
self.assertEqual(self._get_summary_line(C.cmeth),
"cmeth" + bound + " method of builtins.type instance")
"cmeth" + bound + " class method of test.test_pydoc.C")
C.smeth.__text_signature__ = text_signature
self.assertEqual(self._get_summary_line(C.smeth),
"smeth" + unbound)
@ -1460,13 +1465,13 @@ sm(x, y)
'cm(...)\n'
' A class method\n')
self.assertEqual(self._get_summary_lines(X.cm), """\
cm(x) method of builtins.type instance
cm(x) class method of test.test_pydoc.X
A class method
""")
self.assertIn("""
| Class methods defined here:
|
| cm(x) from builtins.type
| cm(x)
| A class method
""", pydoc.plain(pydoc.render_doc(X)))
@ -1623,6 +1628,128 @@ foo
)
class PydocFodderTest(unittest.TestCase):
def getsection(self, text, beginline, endline):
lines = text.splitlines()
beginindex, endindex = 0, None
if beginline is not None:
beginindex = lines.index(beginline)
if endline is not None:
endindex = lines.index(endline, beginindex)
return lines[beginindex:endindex]
def test_text_doc_routines_in_class(self, cls=pydocfodder.B):
doc = pydoc.TextDoc()
result = doc.docclass(cls)
result = clean_text(result)
where = 'defined here' if cls is pydocfodder.B else 'inherited from B'
lines = self.getsection(result, f' | Methods {where}:', ' | ' + '-'*70)
self.assertIn(' | A_method_alias = A_method(self)', lines)
self.assertIn(' | B_method_alias = B_method(self)', lines)
self.assertIn(' | A_staticmethod(x, y) from test.pydocfodder.A', lines)
self.assertIn(' | A_staticmethod_alias = A_staticmethod(x, y)', lines)
self.assertIn(' | global_func(x, y) from test.pydocfodder', lines)
self.assertIn(' | global_func_alias = global_func(x, y)', lines)
self.assertIn(' | global_func2_alias = global_func2(x, y) from test.pydocfodder', lines)
self.assertIn(' | __repr__(self, /) from builtins.object', lines)
self.assertIn(' | object_repr = __repr__(self, /)', lines)
lines = self.getsection(result, f' | Static methods {where}:', ' | ' + '-'*70)
self.assertIn(' | A_classmethod_ref = A_classmethod(x) class method of test.pydocfodder.A', lines)
note = '' if cls is pydocfodder.B else ' class method of test.pydocfodder.B'
self.assertIn(' | B_classmethod_ref = B_classmethod(x)' + note, lines)
self.assertIn(' | A_method_ref = A_method() method of test.pydocfodder.A instance', lines)
self.assertIn(' | get(key, default=None, /) method of builtins.dict instance', lines)
self.assertIn(' | dict_get = get(key, default=None, /) method of builtins.dict instance', lines)
lines = self.getsection(result, f' | Class methods {where}:', ' | ' + '-'*70)
self.assertIn(' | B_classmethod(x)', lines)
self.assertIn(' | B_classmethod_alias = B_classmethod(x)', lines)
def test_html_doc_routines_in_class(self, cls=pydocfodder.B):
doc = pydoc.HTMLDoc()
result = doc.docclass(cls)
result = html2text(result)
where = 'defined here' if cls is pydocfodder.B else 'inherited from B'
lines = self.getsection(result, f'Methods {where}:', '-'*70)
self.assertIn('A_method_alias = A_method(self)', lines)
self.assertIn('B_method_alias = B_method(self)', lines)
self.assertIn('A_staticmethod(x, y) from test.pydocfodder.A', lines)
self.assertIn('A_staticmethod_alias = A_staticmethod(x, y)', lines)
self.assertIn('global_func(x, y) from test.pydocfodder', lines)
self.assertIn('global_func_alias = global_func(x, y)', lines)
self.assertIn('global_func2_alias = global_func2(x, y) from test.pydocfodder', lines)
self.assertIn('__repr__(self, /) from builtins.object', lines)
self.assertIn('object_repr = __repr__(self, /)', lines)
lines = self.getsection(result, f'Static methods {where}:', '-'*70)
self.assertIn('A_classmethod_ref = A_classmethod(x) class method of test.pydocfodder.A', lines)
note = '' if cls is pydocfodder.B else ' class method of test.pydocfodder.B'
self.assertIn('B_classmethod_ref = B_classmethod(x)' + note, lines)
self.assertIn('A_method_ref = A_method() method of test.pydocfodder.A instance', lines)
lines = self.getsection(result, f'Class methods {where}:', '-'*70)
self.assertIn('B_classmethod(x)', lines)
self.assertIn('B_classmethod_alias = B_classmethod(x)', lines)
def test_text_doc_inherited_routines_in_class(self):
self.test_text_doc_routines_in_class(pydocfodder.D)
def test_html_doc_inherited_routines_in_class(self):
self.test_html_doc_routines_in_class(pydocfodder.D)
def test_text_doc_routines_in_module(self):
doc = pydoc.TextDoc()
result = doc.docmodule(pydocfodder)
result = clean_text(result)
lines = self.getsection(result, 'FUNCTIONS', 'FILE')
# function alias
self.assertIn(' global_func_alias = global_func(x, y)', lines)
self.assertIn(' A_staticmethod(x, y)', lines)
self.assertIn(' A_staticmethod_alias = A_staticmethod(x, y)', lines)
# bound class methods
self.assertIn(' A_classmethod(x) class method of A', lines)
self.assertIn(' A_classmethod2 = A_classmethod(x) class method of A', lines)
self.assertIn(' A_classmethod3 = A_classmethod(x) class method of B', lines)
# bound methods
self.assertIn(' A_method() method of A instance', lines)
self.assertIn(' A_method2 = A_method() method of A instance', lines)
self.assertIn(' A_method3 = A_method() method of B instance', lines)
self.assertIn(' A_staticmethod_ref = A_staticmethod(x, y)', lines)
self.assertIn(' A_staticmethod_ref2 = A_staticmethod(y) method of B instance', lines)
self.assertIn(' get(key, default=None, /) method of builtins.dict instance', lines)
self.assertIn(' dict_get = get(key, default=None, /) method of builtins.dict instance', lines)
# unbound methods
self.assertIn(' B_method(self)', lines)
self.assertIn(' B_method2 = B_method(self)', lines)
def test_html_doc_routines_in_module(self):
doc = pydoc.HTMLDoc()
result = doc.docmodule(pydocfodder)
result = html2text(result)
lines = self.getsection(result, ' Functions', None)
# function alias
self.assertIn(' global_func_alias = global_func(x, y)', lines)
self.assertIn(' A_staticmethod(x, y)', lines)
self.assertIn(' A_staticmethod_alias = A_staticmethod(x, y)', lines)
# bound class methods
self.assertIn('A_classmethod(x) class method of A', lines)
self.assertIn(' A_classmethod2 = A_classmethod(x) class method of A', lines)
self.assertIn(' A_classmethod3 = A_classmethod(x) class method of B', lines)
# bound methods
self.assertIn(' A_method() method of A instance', lines)
self.assertIn(' A_method2 = A_method() method of A instance', lines)
self.assertIn(' A_method3 = A_method() method of B instance', lines)
self.assertIn(' A_staticmethod_ref = A_staticmethod(x, y)', lines)
self.assertIn(' A_staticmethod_ref2 = A_staticmethod(y) method of B instance', lines)
self.assertIn(' get(key, default=None, /) method of builtins.dict instance', lines)
self.assertIn(' dict_get = get(key, default=None, /) method of builtins.dict instance', lines)
# unbound methods
self.assertIn(' B_method(self)', lines)
self.assertIn(' B_method2 = B_method(self)', lines)
@unittest.skipIf(
is_emscripten or is_wasi,
"Socket server not available on Emscripten/WASI."

View file

@ -0,0 +1,7 @@
Fix rendering class methods, bound methods, method and function aliases in
:mod:`pydoc`. 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.