https://github.com/python/cpython/commit/70748bdbea872a84dd8eadad9b48c73e218d2e1f
commit: 70748bdbea872a84dd8eadad9b48c73e218d2e1f
branch: main
author: Jacob Austin Lincoln <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2025-11-12T10:07:21Z
summary:

gh-131116: Fix inspect.getdoc() to work with cached_property objects (GH-131165)

files:
A Lib/test/test_inspect/inspect_fodder3.py
A Misc/NEWS.d/next/Library/2025-03-12-18-57-10.gh-issue-131116.uTpwXZ.rst
M Doc/library/inspect.rst
M Lib/inspect.py
M Lib/test/test_inspect/test_inspect.py

diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
index aff53b78c4a774..13a352cbdb2cdc 100644
--- a/Doc/library/inspect.rst
+++ b/Doc/library/inspect.rst
@@ -639,6 +639,9 @@ Retrieving source code
    .. versionchanged:: next
       Added parameters *inherit_class_doc* and *fallback_to_class_doc*.
 
+      Documentation strings on :class:`~functools.cached_property`
+      objects are now inherited if not overriden.
+
 
 .. function:: getcomments(object)
 
diff --git a/Lib/inspect.py b/Lib/inspect.py
index bb17848b444b67..8e7511b3af015f 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -747,6 +747,12 @@ def _finddoc(obj, *, search_in_class=True):
         cls = _findclass(obj.fget)
         if cls is None or getattr(cls, name) is not obj:
             return None
+    # Should be tested before ismethoddescriptor()
+    elif isinstance(obj, functools.cached_property):
+        name = obj.attrname
+        cls = _findclass(obj.func)
+        if cls is None or getattr(cls, name) is not obj:
+            return None
     elif ismethoddescriptor(obj) or isdatadescriptor(obj):
         name = obj.__name__
         cls = obj.__objclass__
diff --git a/Lib/test/test_inspect/inspect_fodder3.py 
b/Lib/test/test_inspect/inspect_fodder3.py
new file mode 100644
index 00000000000000..ea2481edf938c2
--- /dev/null
+++ b/Lib/test/test_inspect/inspect_fodder3.py
@@ -0,0 +1,39 @@
+from functools import cached_property
+
+# docstring in parent, inherited in child
+class ParentInheritDoc:
+    @cached_property
+    def foo(self):
+        """docstring for foo defined in parent"""
+
+class ChildInheritDoc(ParentInheritDoc):
+    pass
+
+class ChildInheritDefineDoc(ParentInheritDoc):
+    @cached_property
+    def foo(self):
+        pass
+
+# Redefine foo as something other than cached_property
+class ChildPropertyFoo(ParentInheritDoc):
+    @property
+    def foo(self):
+        """docstring for the property foo"""
+
+class ChildMethodFoo(ParentInheritDoc):
+    def foo(self):
+        """docstring for the method foo"""
+
+# docstring in child but not parent
+class ParentNoDoc:
+    @cached_property
+    def foo(self):
+        pass
+
+class ChildNoDoc(ParentNoDoc):
+    pass
+
+class ChildDefineDoc(ParentNoDoc):
+    @cached_property
+    def foo(self):
+        """docstring for foo defined in child"""
diff --git a/Lib/test/test_inspect/test_inspect.py 
b/Lib/test/test_inspect/test_inspect.py
index 24fd4a2fa626d4..dd3b7d9c5b4b5b 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -46,6 +46,7 @@
 
 from test.test_inspect import inspect_fodder as mod
 from test.test_inspect import inspect_fodder2 as mod2
+from test.test_inspect import inspect_fodder3 as mod3
 from test.test_inspect import inspect_stringized_annotations
 from test.test_inspect import inspect_deferred_annotations
 
@@ -714,6 +715,25 @@ class B(A):
         b.__doc__ = 'Instance'
         self.assertEqual(inspect.getdoc(b, fallback_to_class_doc=False), 
'Instance')
 
+    def test_getdoc_inherited_cached_property(self):
+        doc = inspect.getdoc(mod3.ParentInheritDoc.foo)
+        self.assertEqual(doc, 'docstring for foo defined in parent')
+        self.assertEqual(inspect.getdoc(mod3.ChildInheritDoc.foo), doc)
+        self.assertEqual(inspect.getdoc(mod3.ChildInheritDefineDoc.foo), doc)
+
+    def test_getdoc_redefine_cached_property_as_other(self):
+        self.assertEqual(inspect.getdoc(mod3.ChildPropertyFoo.foo),
+                         'docstring for the property foo')
+        self.assertEqual(inspect.getdoc(mod3.ChildMethodFoo.foo),
+                         'docstring for the method foo')
+
+    def test_getdoc_define_cached_property(self):
+        self.assertEqual(inspect.getdoc(mod3.ChildDefineDoc.foo),
+                         'docstring for foo defined in child')
+
+    def test_getdoc_nodoc_inherited(self):
+        self.assertIsNone(inspect.getdoc(mod3.ChildNoDoc.foo))
+
     @unittest.skipIf(MISSING_C_DOCSTRINGS, "test requires docstrings")
     def test_finddoc(self):
         finddoc = inspect._finddoc
diff --git 
a/Misc/NEWS.d/next/Library/2025-03-12-18-57-10.gh-issue-131116.uTpwXZ.rst 
b/Misc/NEWS.d/next/Library/2025-03-12-18-57-10.gh-issue-131116.uTpwXZ.rst
new file mode 100644
index 00000000000000..f5e60ab6e8c4cb
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-03-12-18-57-10.gh-issue-131116.uTpwXZ.rst
@@ -0,0 +1,2 @@
+:func:`inspect.getdoc` now correctly returns an inherited docstring on
+:class:`~functools.cached_property` objects if none is given in a subclass.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to