From 13113fc0b65cf4e801e0ee0aacfa0ef6eb4949cc Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 26 Apr 2020 11:24:15 +0900 Subject: [PATCH] Fix #6588: autodoc: Decorated inherited method has no documentation --- CHANGES | 1 + sphinx/ext/autodoc/__init__.py | 12 ++++++++---- sphinx/util/inspect.py | 13 ++++++++++++- tests/test_util_inspect.py | 15 +++++++++++++++ 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 5a5acf6a7..8fedf85db 100644 --- a/CHANGES +++ b/CHANGES @@ -68,6 +68,7 @@ Bugs fixed * #6703: autodoc: incremental build does not work for imported objects * #7564: autodoc: annotations not to be shown for descriptors +* #6588: autodoc: Decorated inherited method has no documentation * #7535: sphinx-autogen: crashes when custom template uses inheritance * #7536: sphinx-autogen: crashes when template uses i18n feature diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index ed717387b..28f1c52c6 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -436,7 +436,8 @@ class Documenter: % self.__class__.__name__, RemovedInSphinx40Warning) docstring = getdoc(self.object, self.get_attr, - self.env.config.autodoc_inherit_docstrings) + self.env.config.autodoc_inherit_docstrings, + self.parent, self.object_name) if docstring: tab_width = self.directive.state.document.settings.tab_width return [prepare_docstring(docstring, ignore, tab_width)] @@ -557,7 +558,8 @@ class Documenter: else: isattr = False - doc = getdoc(member, self.get_attr, self.env.config.autodoc_inherit_docstrings) + doc = getdoc(member, self.get_attr, self.env.config.autodoc_inherit_docstrings, + self.parent, self.object_name) if not isinstance(doc, str): # Ignore non-string __doc__ doc = None @@ -1250,7 +1252,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: if content in ('both', 'init'): __init__ = self.get_attr(self.object, '__init__', None) initdocstring = getdoc(__init__, self.get_attr, - self.env.config.autodoc_inherit_docstrings) + self.env.config.autodoc_inherit_docstrings, + self.parent, self.object_name) # for new-style classes, no __init__ means default __init__ if (initdocstring is not None and (initdocstring == object.__init__.__doc__ or # for pypy @@ -1260,7 +1263,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: # try __new__ __new__ = self.get_attr(self.object, '__new__', None) initdocstring = getdoc(__new__, self.get_attr, - self.env.config.autodoc_inherit_docstrings) + self.env.config.autodoc_inherit_docstrings, + self.parent, self.object_name) # for new-style classes, no __new__ means default __new__ if (initdocstring is not None and (initdocstring == object.__new__.__doc__ or # for pypy diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 0434d7850..38777728a 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -691,13 +691,14 @@ class Signature: def getdoc(obj: Any, attrgetter: Callable = safe_getattr, - allow_inherited: bool = False) -> str: + allow_inherited: bool = False, cls: Any = None, name: str = None) -> str: """Get the docstring for the object. This tries to obtain the docstring for some kind of objects additionally: * partial functions * inherited docstring + * inherited decorated methods """ doc = attrgetter(obj, '__doc__', None) if ispartial(obj) and doc == obj.__class__.__doc__: @@ -705,4 +706,14 @@ def getdoc(obj: Any, attrgetter: Callable = safe_getattr, elif doc is None and allow_inherited: doc = inspect.getdoc(obj) + if doc is None and cls: + # inspect.getdoc() does not support some kind of inherited and decorated methods. + # This tries to obtain the docstring from super classes. + for basecls in getattr(cls, '__mro__', []): + meth = safe_getattr(basecls, name, None) + if meth: + doc = inspect.getdoc(meth) + if doc: + break + return doc diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index ff1074702..65070d6d1 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -564,3 +564,18 @@ def test_unpartial(): assert inspect.unpartial(func2) is func1 assert inspect.unpartial(func3) is func1 + + +def test_getdoc_inherited_decorated_method(): + class Foo: + def meth(self): + """docstring.""" + + class Bar(Foo): + @functools.lru_cache() + def meth(self): + # inherited and decorated method + pass + + assert inspect.getdoc(Bar.meth, getattr, False, Bar, "meth") is None + assert inspect.getdoc(Bar.meth, getattr, True, Bar, "meth") == "docstring."