From 70748bdbea872a84dd8eadad9b48c73e218d2e1f Mon Sep 17 00:00:00 2001 From: Jacob Austin Lincoln <99031153+lincolnj1@users.noreply.github.com> Date: Wed, 12 Nov 2025 02:07:21 -0800 Subject: [PATCH] gh-131116: Fix inspect.getdoc() to work with cached_property objects (GH-131165) --- Doc/library/inspect.rst | 3 ++ Lib/inspect.py | 6 +++ Lib/test/test_inspect/inspect_fodder3.py | 39 +++++++++++++++++++ Lib/test/test_inspect/test_inspect.py | 20 ++++++++++ ...-03-12-18-57-10.gh-issue-131116.uTpwXZ.rst | 2 + 5 files changed, 70 insertions(+) create mode 100644 Lib/test/test_inspect/inspect_fodder3.py create mode 100644 Misc/NEWS.d/next/Library/2025-03-12-18-57-10.gh-issue-131116.uTpwXZ.rst diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index aff53b78c4a7..13a352cbdb2c 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 bb17848b444b..8e7511b3af01 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 000000000000..ea2481edf938 --- /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 24fd4a2fa626..dd3b7d9c5b4b 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -46,6 +46,7 @@ from test import support 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 TestRetrievingSourceCode(GetSourceBase): 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 000000000000..f5e60ab6e8c4 --- /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. -- 2.47.3