`inspect.getdoc()` failing to return docstring for cached_property for subclass that overrides code but not docstring · Issue #131116 · python/cpython · GitHub
When a child/sub-class inherits a method from its parent/base-class, inspect.getdoc(...) returns the parent's method's docstring when the child's method does not have its own docstring. This is true even if the child overwrites the method but doesn't have a docstring as part of that.
However, this is failing when the method has the functools.cached_property decorator on it and the method is overwritten in the child. The below code errors on the assertion.
fromfunctoolsimportcached_propertyimportinspectclassParent:
@cached_propertydeffoo(self):
"this is the docstring for foo"return1classChild(Parent):
@cached_propertydeffoo(self): ...
assertinspect.getdoc(Child.foo) ==inspect.getdoc(Parent.foo) # fails but we want this to be True
I did my best to test this across a variety of similar things and this was the only case where I found the docstring to not be inherited. I tested across methods, properties (with the @property decorator), cached_properties, static_methods, and class_methods; and having the child explicitly overwrite the code (but not the docstring) of the method or not.
The below test code shows passes for all those other scenarios but only fails on test_cached_property_defined_in_parent_and_child.
fromfunctoolsimportcached_propertyimportinspectimportunittestclassTestMatchingDocstrings(unittest.TestCase):
defsetUp(self):
self.parent_class=ParentClassself.child_class=ChildClassdefcompare_docstrings(self, attrname):
parent_attr=getattr(self.parent_class, attrname)
parent_docstring=inspect.getdoc(parent_attr)
child_attr=getattr(self.child_class, attrname)
child_docstring=inspect.getdoc(child_attr)
self.assertEqual(parent_docstring, child_docstring)
deftest_method_defined_in_parent_only(self):
self.compare_docstrings("method_defined_in_parent_only")
deftest_method_defined_in_parent_and_child(self):
self.compare_docstrings("method_defined_in_parent_and_child")
deftest_property_defined_in_parent_only(self):
self.compare_docstrings("property_defined_in_parent_only")
deftest_property_defined_in_parent_and_child(self):
self.compare_docstrings("property_defined_in_parent_and_child")
deftest_cached_property_defined_in_parent_only(self):
self.compare_docstrings("cached_property_defined_in_parent_only")
deftest_cached_property_defined_in_parent_and_child(self):
self.compare_docstrings("cached_property_defined_in_parent_and_child")
deftest_static_method_defined_in_parent_only(self):
self.compare_docstrings("static_method_defined_in_parent_only")
deftest_static_method_defined_in_parent_and_child(self):
self.compare_docstrings("static_method_defined_in_parent_and_child")
deftest_class_method_defined_in_parent_only(self):
self.compare_docstrings("class_method_defined_in_parent_only")
deftest_class_method_defined_in_parent_and_child(self):
self.compare_docstrings("class_method_defined_in_parent_and_child")
classParentClass:
defmethod_defined_in_parent_only(self):
"This is a method in the parent only"defmethod_defined_in_parent_and_child(self):
"This is a method in the parent and child"@propertydefproperty_defined_in_parent_only(self):
"This is a property with the decorator in the parent only"@propertydefproperty_defined_in_parent_and_child(self):
"This is a property with the decorator in the parent and child"@cached_propertydefcached_property_defined_in_parent_only(self):
"This is a cached property with the decorator in the parent only"@cached_propertydefcached_property_defined_in_parent_and_child(self):
"This is a cached property with the decorator in the parent and child"@staticmethoddefstatic_method_defined_in_parent_only():
"This is a static method with the decorator in the parent only"@staticmethoddefstatic_method_defined_in_parent_and_child():
"This is a static method with the decorator in the parent and child"@classmethoddefclass_method_defined_in_parent_only(cls):
"This is a class method with the decorator in the parent only"@classmethoddefclass_method_defined_in_parent_and_child(cls):
"This is a class method with the decorator in the parent and child"classChildClass(ParentClass):
defmethod_defined_in_parent_and_child(self): ...
@propertydefproperty_defined_in_parent_and_child(self): ...
@cached_propertydefcached_property_defined_in_parent_and_child(self): ...
@staticmethoddefstatic_method_defined_in_parent_and_child(): ...
@classmethoddefclass_method_defined_in_parent_and_child(cls): ...
if__name__=="__main__":
unittest.main()
I'd like to work on this if no one else is. Not sure how to assign these.
We don't assign issues in general. Just say that you want to work on it and we should assume that's the case. If you want to drop the task, you should just say so and others will know that the task is now "available".
interestingly enough, pyright (or pylance? the vscode extension for python from microsoft) is able to show the correct docstring for the child cached_property
They may be using something else and not rely on inspect (there are multiple ways of extracting the docstring and they may fallback to others if they can't find one maybe?)
It definitely seems like a problem in inspect. When you call inspect.getdoc(Child.foo), it goes to the _finddoc() method and follows the "ismethoddescriptor" branch, then errors on accessing Child.foo.__name__ (no such attribute).
Property has a special branch in _finddoc since it's a weird datadescriptor (and would otherwise follow that same branch and cause a similar error). Seems like cached_property needs a similar fix.
It definitely seems like a problem in inspect. When you call inspect.getdoc(Child.foo), it goes to the _finddoc() method and follows the "ismethoddescriptor" branch, then errors on accessing Child.foo.name (no such attribute).
Property has a special branch in _finddoc since it's a weird datadescriptor (and would otherwise follow that same branch and cause a similar error). Seems like cached_property needs a similar fix.
That seems reasonable to me (note that I first looked at any of this code yesterday).
It looks like in each of those conditional groups, the goal is to get name and cls, and it seems like we can get them both from obj.func rather than obj itself.
Activity
lincolnj1 commented on Mar 12, 2025
I'd like to work on this if no one else is. Not sure how to assign these.
sergey-miryanov commented on Mar 12, 2025
As I understand you don't need to assign it - just do you best to work on it.
picnixz commented on Mar 12, 2025
We don't assign issues in general. Just say that you want to work on it and we should assume that's the case. If you want to drop the task, you should just say so and others will know that the task is now "available".
scotscotmcc commented on Mar 12, 2025
interestingly enough, pyright (or pylance? the vscode extension for python from microsoft) is able to show the correct docstring for the child cached_property
picnixz commented on Mar 12, 2025
They may be using something else and not rely on inspect (there are multiple ways of extracting the docstring and they may fallback to others if they can't find one maybe?)
lincolnj1 commented on Mar 12, 2025
It definitely seems like a problem in inspect. When you call inspect.getdoc(Child.foo), it goes to the _finddoc() method and follows the "ismethoddescriptor" branch, then errors on accessing Child.foo.__name__ (no such attribute).
Property has a special branch in _finddoc since it's a weird datadescriptor (and would otherwise follow that same branch and cause a similar error). Seems like cached_property needs a similar fix.
scotscotmcc commented on Mar 12, 2025
That seems reasonable to me (note that I first looked at any of this code yesterday).
It looks like in each of those conditional groups, the goal is to get name and cls, and it seems like we can get them both from obj.func rather than obj itself.
attempted fix of issue python#131116. Test case failing on non change…
gh-131116: Fix inspect.getdoc() to work with cached_property objects (G…
pythongh-131116: Fix inspect.getdoc() to work with cached_property ob…