From cfb79caaab6c4823b6b186aabab642c4dab3927f Mon Sep 17 00:00:00 2001
From: Serhiy Storchaka <storchaka@gmail.com>
Date: Sun, 11 Feb 2024 15:56:34 +0200
Subject: [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 2939ad02be62110ffa2ac6c4d9211c85e1d1720f)
---
 Lib/pydoc.py                                       | 149 ++++++++++++++-----
 Lib/test/pydocfodder.py                            |  48 ++++++-
 Lib/test/test_enum.py                              |  10 +-
 Lib/test/test_pydoc.py                             | 160 +++++++++++++++++++--
 .../2024-01-11-15-10-53.gh-issue-97959.UOj6d4.rst  |   7 +
 5 files changed, 322 insertions(+), 52 deletions(-)
 create mode 100644 Misc/NEWS.d/next/Library/2024-01-11-15-10-53.gh-issue-97959.UOj6d4.rst

diff --git a/Lib/pydoc.py b/Lib/pydoc.py
index 84bbf58..63b4e74 100755
--- a/Lib/pydoc.py
+++ b/Lib/pydoc.py
@@ -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
@@ -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
 
@@ -658,6 +673,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__)
@@ -902,7 +936,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
 
@@ -1025,24 +1059,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)):
@@ -1053,10 +1107,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' % (
@@ -1089,7 +1146,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
@@ -1200,7 +1257,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))
@@ -1384,7 +1441,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):
@@ -1459,23 +1516,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)):
@@ -1486,8 +1563,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
 
@@ -1514,7 +1594,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
@@ -1530,7 +1610,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:
diff --git a/Lib/test/pydocfodder.py b/Lib/test/pydocfodder.py
index a3ef223..27037e0 100644
--- a/Lib/test/pydocfodder.py
+++ b/Lib/test/pydocfodder.py
@@ -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
diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py
index 08c7dde..99e2913 100644
--- a/Lib/test/test_enum.py
+++ b/Lib/test/test_enum.py
@@ -4752,22 +4752,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)
  |
  |  ----------------------------------------------------------------------
diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py
index dbd86a6..87cd6d5 100644
--- a/Lib/test/test_pydoc.py
+++ b/Lib/test/test_pydoc.py
@@ -22,6 +22,7 @@ import textwrap
 from io import StringIO
 from collections import namedtuple
 from urllib.request import urlopen, urlcleanup
+from test import support
 from test.support import import_helper
 from test.support import os_helper
 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)
 from test.support.os_helper import (TESTFN, rmtree, unlink)
 from test import pydoc_mod
+from test import pydocfodder
 
 
 class nonascii:
@@ -99,7 +101,7 @@ CLASSES
      |  ----------------------------------------------------------------------
      |  Class methods defined here:
      |
-     |  __class_getitem__(item) from builtins.type
+     |  __class_getitem__(item)
      |
      |  ----------------------------------------------------------------------
      |  Data descriptors defined here:
@@ -163,7 +165,7 @@ class A(builtins.object)
     Methods defined here:
         __init__()
             Wow, I have no function!
-
+    ----------------------------------------------------------------------
     Data descriptors defined here:
         __dict__
             dictionary for instance variables
@@ -176,6 +178,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'>}
@@ -188,8 +191,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
@@ -327,6 +332,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
@@ -344,10 +353,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):
@@ -364,6 +370,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
@@ -791,8 +798,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
 
@@ -1147,7 +1153,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):
@@ -1183,6 +1189,14 @@ class TestDescriptions(unittest.TestCase):
         self.assertEqual(self._get_summary_line(os.stat),
             "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
     def test_staticmethod(self):
         class X:
@@ -1215,13 +1229,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)))
 
@@ -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(
     is_emscripten or is_wasi,
     "Socket server not available on Emscripten/WASI."
diff --git a/Misc/NEWS.d/next/Library/2024-01-11-15-10-53.gh-issue-97959.UOj6d4.rst b/Misc/NEWS.d/next/Library/2024-01-11-15-10-53.gh-issue-97959.UOj6d4.rst
new file mode 100644
index 0000000..a317271
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-01-11-15-10-53.gh-issue-97959.UOj6d4.rst
@@ -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.
-- 
cgit v0.12