[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 2939ad02be)
This commit is contained in:
Serhiy Storchaka 2024-02-11 15:56:34 +02:00 committed by GitHub
parent d8346d6c06
commit cfb79caaab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 322 additions and 52 deletions

View file

@ -204,6 +204,19 @@ def classname(object, modname):
name = object.__module__ + '.' + name name = object.__module__ + '.' + name
return 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): def isdata(object):
"""Check if an object is of a type that probably means it's data.""" """Check if an object is of a type that probably means it's data."""
return not (inspect.ismodule(object) or inspect.isclass(object) or return not (inspect.ismodule(object) or inspect.isclass(object) or
@ -298,13 +311,15 @@ def visiblename(name, all=None, obj=None):
return not name.startswith('_') return not name.startswith('_')
def classify_class_attrs(object): 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 = [] results = []
for (name, kind, cls, value) in inspect.classify_class_attrs(object): for (name, kind, cls, value) in inspect.classify_class_attrs(object):
if inspect.isdatadescriptor(value): if inspect.isdatadescriptor(value):
kind = 'data descriptor' kind = 'data descriptor'
if isinstance(value, property) and value.fset is None: if isinstance(value, property) and value.fset is None:
kind = 'readonly property' kind = 'readonly property'
elif kind == 'method' and _is_bound_method(value):
kind = 'static method'
results.append((name, kind, cls, value)) results.append((name, kind, cls, value))
return results return results
@ -658,6 +673,25 @@ class HTMLDoc(Doc):
module.__name__, name, classname(object, modname)) module.__name__, name, classname(object, modname))
return 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): def modulelink(self, object):
"""Make a link for a module.""" """Make a link for a module."""
return '<a href="%s.html">%s</a>' % (object.__name__, object.__name__) return '<a href="%s.html">%s</a>' % (object.__name__, object.__name__)
@ -902,7 +936,7 @@ class HTMLDoc(Doc):
push(self.docdata(value, name, mod)) push(self.docdata(value, name, mod))
else: else:
push(self.document(value, name, mod, push(self.document(value, name, mod,
funcs, classes, mdict, object)) funcs, classes, mdict, object, homecls))
push('\n') push('\n')
return attrs return attrs
@ -1025,24 +1059,44 @@ class HTMLDoc(Doc):
return self.grey('=' + self.repr(object)) return self.grey('=' + self.repr(object))
def docroutine(self, object, name=None, mod=None, 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.""" """Produce HTML documentation for a function or method object."""
realname = object.__name__ realname = object.__name__
name = name or realname 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 = '' note = ''
skipdocs = 0 skipdocs = False
imfunc = None
if _is_bound_method(object): if _is_bound_method(object):
imclass = object.__self__.__class__ imself = object.__self__
if cl: if imself is cl:
if imclass is not cl: imfunc = getattr(object, '__func__', None)
note = ' from ' + self.classlink(imclass, mod) elif inspect.isclass(imself):
note = ' class method of %s' % self.classlink(imself, mod)
else: else:
if object.__self__ is not None: note = ' method of %s instance' % self.classlink(
note = ' method of %s instance' % self.classlink( imself.__class__, mod)
object.__self__.__class__, mod) elif (inspect.ismethoddescriptor(object) or
else: inspect.ismethodwrapper(object)):
note = ' unbound %s method' % self.classlink(imclass,mod) 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 if (inspect.iscoroutinefunction(object) or
inspect.isasyncgenfunction(object)): inspect.isasyncgenfunction(object)):
@ -1053,10 +1107,13 @@ class HTMLDoc(Doc):
if name == realname: if name == realname:
title = '<a name="%s"><strong>%s</strong></a>' % (anchor, realname) title = '<a name="%s"><strong>%s</strong></a>' % (anchor, realname)
else: 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>' % ( reallink = '<a href="#%s">%s</a>' % (
cl.__name__ + '-' + realname, realname) cl.__name__ + '-' + realname, realname)
skipdocs = 1 skipdocs = True
if note.startswith(' from '):
note = ''
else: else:
reallink = realname reallink = realname
title = '<a name="%s"><strong>%s</strong></a> = %s' % ( title = '<a name="%s"><strong>%s</strong></a> = %s' % (
@ -1089,7 +1146,7 @@ class HTMLDoc(Doc):
doc = doc and '<dd><span class="code">%s</span></dd>' % doc doc = doc and '<dd><span class="code">%s</span></dd>' % doc
return '<dl><dt>%s</dt>%s</dl>\n' % (decl, 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.""" """Produce html documentation for a data descriptor."""
results = [] results = []
push = results.append push = results.append
@ -1200,7 +1257,7 @@ class TextDoc(Doc):
entry, modname, c, prefix + ' ') entry, modname, c, prefix + ' ')
return result 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.""" """Produce text documentation for a given module object."""
name = object.__name__ # ignore the passed-in name name = object.__name__ # ignore the passed-in name
synop, desc = splitdoc(getdoc(object)) synop, desc = splitdoc(getdoc(object))
@ -1384,7 +1441,7 @@ location listed above.
push(self.docdata(value, name, mod)) push(self.docdata(value, name, mod))
else: else:
push(self.document(value, push(self.document(value,
name, mod, object)) name, mod, object, homecls))
return attrs return attrs
def spilldescriptors(msg, attrs, predicate): def spilldescriptors(msg, attrs, predicate):
@ -1459,23 +1516,43 @@ location listed above.
"""Format an argument default value as text.""" """Format an argument default value as text."""
return '=' + self.repr(object) 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.""" """Produce text documentation for a function or method object."""
realname = object.__name__ realname = object.__name__
name = name or realname name = name or realname
if homecls is None:
homecls = cl
note = '' note = ''
skipdocs = 0 skipdocs = False
imfunc = None
if _is_bound_method(object): if _is_bound_method(object):
imclass = object.__self__.__class__ imself = object.__self__
if cl: if imself is cl:
if imclass is not cl: imfunc = getattr(object, '__func__', None)
note = ' from ' + classname(imclass, mod) elif inspect.isclass(imself):
note = ' class method of %s' % classname(imself, mod)
else: else:
if object.__self__ is not None: note = ' method of %s instance' % classname(
note = ' method of %s instance' % classname( imself.__class__, mod)
object.__self__.__class__, mod) elif (inspect.ismethoddescriptor(object) or
else: inspect.ismethodwrapper(object)):
note = ' unbound %s method' % classname(imclass,mod) 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 if (inspect.iscoroutinefunction(object) or
inspect.isasyncgenfunction(object)): inspect.isasyncgenfunction(object)):
@ -1486,8 +1563,11 @@ location listed above.
if name == realname: if name == realname:
title = self.bold(realname) title = self.bold(realname)
else: else:
if cl and inspect.getattr_static(cl, realname, []) is object: if (cl is not None and
skipdocs = 1 inspect.getattr_static(cl, realname, []) is object):
skipdocs = True
if note.startswith(' from '):
note = ''
title = self.bold(name) + ' = ' + realname title = self.bold(name) + ' = ' + realname
argspec = None argspec = None
@ -1514,7 +1594,7 @@ location listed above.
doc = getdoc(object) or '' doc = getdoc(object) or ''
return decl + '\n' + (doc and self.indent(doc).rstrip() + '\n') 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.""" """Produce text documentation for a data descriptor."""
results = [] results = []
push = results.append push = results.append
@ -1530,7 +1610,8 @@ location listed above.
docproperty = docdata 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.""" """Produce text documentation for a data object."""
repr = self.repr(object) repr = self.repr(object)
if maxlen: if maxlen:

View file

@ -2,6 +2,12 @@
import types import types
def global_func(x, y):
"""Module global function"""
def global_func2(x, y):
"""Module global function 2"""
class A: class A:
"A class." "A class."
@ -26,7 +32,7 @@ class A:
"A class method defined in A." "A class method defined in A."
A_classmethod = classmethod(A_classmethod) A_classmethod = classmethod(A_classmethod)
def A_staticmethod(): def A_staticmethod(x, y):
"A static method defined in A." "A static method defined in A."
A_staticmethod = staticmethod(A_staticmethod) A_staticmethod = staticmethod(A_staticmethod)
@ -61,6 +67,28 @@ class B(A):
def BCD_method(self): def BCD_method(self):
"Method defined in B, C and D." "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): class C(A):
"A class, derived from A." "A class, derived from A."
@ -136,3 +164,21 @@ class FunkyProperties(object):
submodule = types.ModuleType(__name__ + '.submodule', submodule = types.ModuleType(__name__ + '.submodule',
"""A submodule, which should appear in its parent's summary""") """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

@ -4752,22 +4752,22 @@ class Color(enum.Enum)
| The value of the Enum member. | 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`. | Return True if `value` is in `cls`.
| |
| `value` is in `cls` if: | `value` is in `cls` if:
| 1) `value` is a member of `cls`, or | 1) `value` is a member of `cls`, or
| 2) `value` is the value of one of the `cls`'s members. | 2) `value` is the value of one of the `cls`'s members.
| |
| __getitem__(name) from enum.EnumType | __getitem__(name)
| Return the member matching `name`. | Return the member matching `name`.
| |
| __iter__() from enum.EnumType | __iter__()
| Return members in definition order. | Return members in definition order.
| |
| __len__() from enum.EnumType | __len__()
| Return the number of members (no aliases) | Return the number of members (no aliases)
| |
| ---------------------------------------------------------------------- | ----------------------------------------------------------------------

View file

@ -22,6 +22,7 @@ import textwrap
from io import StringIO from io import StringIO
from collections import namedtuple from collections import namedtuple
from urllib.request import urlopen, urlcleanup from urllib.request import urlopen, urlcleanup
from test import support
from test.support import import_helper from test.support import import_helper
from test.support import os_helper from test.support import os_helper
from test.support.script_helper import (assert_python_ok, from test.support.script_helper import (assert_python_ok,
@ -32,6 +33,7 @@ from test.support import (reap_children, captured_output, captured_stdout,
requires_docstrings, MISSING_C_DOCSTRINGS) requires_docstrings, MISSING_C_DOCSTRINGS)
from test.support.os_helper import (TESTFN, rmtree, unlink) from test.support.os_helper import (TESTFN, rmtree, unlink)
from test import pydoc_mod from test import pydoc_mod
from test import pydocfodder
class nonascii: class nonascii:
@ -99,7 +101,7 @@ CLASSES
| ---------------------------------------------------------------------- | ----------------------------------------------------------------------
| Class methods defined here: | Class methods defined here:
| |
| __class_getitem__(item) from builtins.type | __class_getitem__(item)
| |
| ---------------------------------------------------------------------- | ----------------------------------------------------------------------
| Data descriptors defined here: | Data descriptors defined here:
@ -163,7 +165,7 @@ class A(builtins.object)
Methods defined here: Methods defined here:
__init__() __init__()
Wow, I have no function! Wow, I have no function!
----------------------------------------------------------------------
Data descriptors defined here: Data descriptors defined here:
__dict__ __dict__
dictionary for instance variables dictionary for instance variables
@ -176,6 +178,7 @@ class B(builtins.object)
dictionary for instance variables dictionary for instance variables
__weakref__ __weakref__
list of weak references to the object list of weak references to the object
----------------------------------------------------------------------
Data and other attributes defined here: Data and other attributes defined here:
NO_MEANING = 'eggs' NO_MEANING = 'eggs'
__annotations__ = {'NO_MEANING': <class 'str'>} __annotations__ = {'NO_MEANING': <class 'str'>}
@ -188,8 +191,10 @@ class C(builtins.object)
is_it_true(self) is_it_true(self)
Return self.get_answer() Return self.get_answer()
say_no(self) say_no(self)
----------------------------------------------------------------------
Class methods defined here: Class methods defined here:
__class_getitem__(item) from builtins.type __class_getitem__(item)
----------------------------------------------------------------------
Data descriptors defined here: Data descriptors defined here:
__dict__ __dict__
dictionary for instance variables dictionary for instance variables
@ -327,6 +332,10 @@ def get_pydoc_html(module):
loc = "<br><a href=\"" + loc + "\">Module Docs</a>" loc = "<br><a href=\"" + loc + "\">Module Docs</a>"
return output.strip(), loc 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): def get_pydoc_link(module):
"Returns a documentation web link of a module" "Returns a documentation web link of a module"
abspath = os.path.abspath abspath = os.path.abspath
@ -344,10 +353,7 @@ def get_pydoc_text(module):
loc = "\nMODULE DOCS\n " + loc + "\n" loc = "\nMODULE DOCS\n " + loc + "\n"
output = doc.docmodule(module) output = doc.docmodule(module)
output = clean_text(output)
# clean up the extra text formatting that pydoc performs
patt = re.compile('\b.')
output = patt.sub('', output)
return output.strip(), loc return output.strip(), loc
def get_html_title(text): def get_html_title(text):
@ -364,6 +370,7 @@ def html2text(html):
Tailored for pydoc tests only. Tailored for pydoc tests only.
""" """
html = html.replace("<dd>", "\n") html = html.replace("<dd>", "\n")
html = html.replace("<hr>", "-"*70)
html = re.sub("<.*?>", "", html) html = re.sub("<.*?>", "", html)
html = pydoc.replace(html, "&nbsp;", " ", "&gt;", ">", "&lt;", "<") html = pydoc.replace(html, "&nbsp;", " ", "&gt;", ">", "&lt;", "<")
return html return html
@ -791,8 +798,7 @@ class PydocDocTest(unittest.TestCase):
b_size = A.a_size b_size = A.a_size
doc = pydoc.render_doc(B) doc = pydoc.render_doc(B)
# clean up the extra text formatting that pydoc performs doc = clean_text(doc)
doc = re.sub('\b.', '', doc)
self.assertEqual(doc, '''\ self.assertEqual(doc, '''\
Python Library Documentation: class B in module %s Python Library Documentation: class B in module %s
@ -1147,7 +1153,7 @@ class TestDescriptions(unittest.TestCase):
@requires_docstrings @requires_docstrings
def test_unbound_builtin_method(self): def test_unbound_builtin_method(self):
self.assertEqual(self._get_summary_line(_pickle.Pickler.dump), self.assertEqual(self._get_summary_line(_pickle.Pickler.dump),
"dump(self, obj, /)") "dump(self, obj, /) unbound _pickle.Pickler method")
# these no longer include "self" # these no longer include "self"
def test_bound_python_method(self): def test_bound_python_method(self):
@ -1183,6 +1189,14 @@ class TestDescriptions(unittest.TestCase):
self.assertEqual(self._get_summary_line(os.stat), self.assertEqual(self._get_summary_line(os.stat),
"stat(path, *, dir_fd=None, follow_symlinks=True)") "stat(path, *, dir_fd=None, follow_symlinks=True)")
def test_unbound_builtin_method_noargs(self):
self.assertEqual(self._get_summary_line(str.lower),
"lower(self, /) unbound builtins.str method")
def test_bound_builtin_method_noargs(self):
self.assertEqual(self._get_summary_line(''.lower),
"lower() method of builtins.str instance")
@requires_docstrings @requires_docstrings
def test_staticmethod(self): def test_staticmethod(self):
class X: class X:
@ -1215,13 +1229,13 @@ sm(x, y)
'cm(...)\n' 'cm(...)\n'
' A class method\n') ' A class method\n')
self.assertEqual(self._get_summary_lines(X.cm), """\ 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 A class method
""") """)
self.assertIn(""" self.assertIn("""
| Class methods defined here: | Class methods defined here:
| |
| cm(x) from builtins.type | cm(x)
| A class method | A class method
""", pydoc.plain(pydoc.render_doc(X))) """, pydoc.plain(pydoc.render_doc(X)))
@ -1378,6 +1392,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( @unittest.skipIf(
is_emscripten or is_wasi, is_emscripten or is_wasi,
"Socket server not available on Emscripten/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.