diff --git a/CHANGES b/CHANGES index 64410011e3a..21824440daa 100644 --- a/CHANGES +++ b/CHANGES @@ -55,6 +55,8 @@ Bugs fixed undocumented * #9185: autodoc: typehints for overloaded functions and methods are inaccurate * #9250: autodoc: The inherited method not having docstring is wrongly parsed +* #9250: autodoc: :confval:`autodoc_inherited_docstrings` does not effect to the + docstring of classes * #9217: manpage: The name of manpage directory that is generated by :confval:`man_make_section_directory` is not correct * #9224: ``:param:`` and ``:type:`` fields does not support a type containing diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index ec1472e202c..fb02484e8ea 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -31,8 +31,8 @@ from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.util import inspect, logging from sphinx.util.docstrings import prepare_docstring, separate_metadata -from sphinx.util.inspect import (evaluate_signature, getdoc, object_description, safe_getattr, - stringify_signature) +from sphinx.util.inspect import (evaluate_signature, getclassdoc, getdoc, object_description, + safe_getattr, stringify_signature) from sphinx.util.typing import OptionSpec, get_type_hints, restify from sphinx.util.typing import stringify as stringify_typehint @@ -1694,9 +1694,9 @@ def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]: classdoc_from = self.options.get('class-doc-from', self.config.autoclass_content) docstrings = [] - attrdocstring = self.get_attr(self.object, '__doc__', None) - if attrdocstring: - docstrings.append(attrdocstring) + classdocstring = getclassdoc(self.object, self.config.autodoc_inherit_docstrings) + if classdocstring: + docstrings.append(classdocstring) # for classes, what the "docstring" is can be controlled via a # config value; the default is only the class docstring diff --git a/sphinx/ext/autodoc/mock.py b/sphinx/ext/autodoc/mock.py index b562f47fd71..34e0960de20 100644 --- a/sphinx/ext/autodoc/mock.py +++ b/sphinx/ext/autodoc/mock.py @@ -165,10 +165,18 @@ def ismock(subject: Any) -> bool: if isinstance(subject, _MockModule): return True + try: + # check the object is a mock class + __mro__ = safe_getattr(subject, '__mro__', []) + if len(__mro__) > 2 and __mro__[1] is _MockObject: + return True + except AttributeError: + pass + try: # check the object is mocked object __mro__ = safe_getattr(type(subject), '__mro__', []) - if len(__mro__) > 2 and __mro__[1] is _MockObject: +: if len(__mro__) > 2 and __mro__[1] is _MockObject: return True except AttributeError: pass diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index a415a7074c8..0201cac93a6 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -824,6 +824,24 @@ def signature_from_ast(node: ast.FunctionDef, code: str = '') -> inspect.Signatu return inspect.Signature(params, return_annotation=return_annotation) +def getclassdoc(obj: Type, allow_inherited: bool = False) -> Optional[str]: + """Get the docstring for the class.""" + from sphinx.ext.autodoc.mock import ismock + + if not allow_inherited: + return safe_getattr(obj, '__doc__', None) + else: + for basecls in getmro(obj)[:-1]: + if ismock(basecls): + break + + doc = safe_getattr(basecls, '__doc__', None) + if doc is not None: + return doc + + return None + + def getdoc(obj: Any, attrgetter: Callable = safe_getattr, allow_inherited: bool = False, cls: Any = None, name: str = None) -> str: """Get the docstring for the object. diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index 4c16886b3fc..aa9a7d7925a 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -2296,6 +2296,8 @@ def test_overload2(app): ' Baz(x: str, y: str)', ' :module: target.overload2', '', + ' docstring', + '', ] diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index de4ad92366b..6c51c550c83 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -671,6 +671,29 @@ def func1(a, b, c): assert inspect.unpartial(func3) is func1 +def test_getclassdoc(): + class Foo: + """docstring""" + + class Bar(Foo): + pass + + assert inspect.getclassdoc(Foo) == "docstring" + assert inspect.getclassdoc(Bar) is None + assert inspect.getclassdoc(Bar, True) == "docstring" + + +def test_getclassdoc_mocked(): + from sphinx.ext.autodoc.mock import _MockModule + + dummy = _MockModule('dummy') + + class Foo(dummy.Dummy): # type: ignore + pass + + assert inspect.getclassdoc(Foo) is None + + def test_getdoc_inherited_decorated_method(): class Foo: def meth(self):