diff --git a/AUTHORS b/AUTHORS index 580feeb32..96d08788f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -57,7 +57,7 @@ Other contributors, listed alphabetically, are: * Stefan Seefeld -- toctree improvements * Gregory Szorc -- performance improvements * Taku Shimizu -- epub3 builder -* Antonio Valentino -- qthelp builder +* Antonio Valentino -- qthelp builder, docstring inheritance * Filip Vavera -- napoleon todo directive * Pauli Virtanen -- autodoc improvements, autosummary extension * Stefan van der Walt -- autosummary extension diff --git a/CHANGES b/CHANGES index 6eaf62af2..bee7cbf32 100644 --- a/CHANGES +++ b/CHANGES @@ -28,6 +28,7 @@ Features added - ``ref.python`` (ref: #3866) * #3872: Add latex key to configure literal blocks caption position in PDF output (refs #3792, #1723) +* In case of missing docstring try to retrieve doc from base classes (ref: #3140) Features removed diff --git a/doc/ext/autodoc.rst b/doc/ext/autodoc.rst index 1f1892dbf..bfd55c81a 100644 --- a/doc/ext/autodoc.rst +++ b/doc/ext/autodoc.rst @@ -393,6 +393,16 @@ There are also new config values that you can set: If ``False`` is given, autodoc forcely suppresses the error if the imported module emits warnings. By default, ``True``. +.. confval:: autodoc_inherit_docstrings + + This value controls the docstrings inheritance. + If set to True the cocstring for classes or methods, if not explicitly set, + is inherited form parents. + + The default is ``True``. + + .. versionadded:: 1.7 + Docstring preprocessing ----------------------- diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 967cd9c5a..0af344751 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -35,7 +35,7 @@ from sphinx.util import logging from sphinx.util.nodes import nested_parse_with_titles from sphinx.util.inspect import Signature, isdescriptor, safe_getmembers, \ safe_getattr, object_description, is_builtin_class_method, \ - isenumclass, isenumattribute + isenumclass, isenumattribute, getdoc from sphinx.util.docstrings import prepare_docstring if False: @@ -525,6 +525,8 @@ class Documenter(object): # type: (unicode, int) -> List[List[unicode]] """Decode and return lines of the docstring(s) for the object.""" docstring = self.get_attr(self.object, '__doc__', None) + if docstring is None and self.env.config.autodoc_inherit_docstrings: + docstring = getdoc(self.object) # make sure we have Unicode docstrings, then sanitize and split # into lines if isinstance(docstring, text_type): @@ -682,6 +684,9 @@ class Documenter(object): isattr = False doc = self.get_attr(member, '__doc__', None) + if doc is None and self.env.config.autodoc_inherit_docstrings: + doc = getdoc(member) + # if the member __doc__ is the same as self's __doc__, it's just # inherited and therefore not the member's doc cls = self.get_attr(member, '__class__', None) @@ -1617,6 +1622,7 @@ def setup(app): app.add_config_value('autodoc_docstring_signature', True, True) app.add_config_value('autodoc_mock_imports', [], True) app.add_config_value('autodoc_warningiserror', True, True) + app.add_config_value('autodoc_inherit_docstrings', True, True) app.add_event('autodoc-process-docstring') app.add_event('autodoc-process-signature') app.add_event('autodoc-skip-member') diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 3f95dfcfe..a2928fc7e 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -11,6 +11,7 @@ from __future__ import absolute_import import re +import sys import typing import inspect from collections import OrderedDict @@ -456,3 +457,102 @@ class Signature(object): ', '.join(param_strings)) return qualified_name + + +if sys.version_info >= (3, 5): + getdoc = inspect.getdoc +else: + # code copied from the inspect.py module of the standard library + # of Python 3.5 + + def _findclass(func): + cls = sys.modules.get(func.__module__) + if cls is None: + return None + if hasattr(func, 'im_class'): + cls = func.im_class + else: + for name in func.__qualname__.split('.')[:-1]: + cls = getattr(cls, name) + if not inspect.isclass(cls): + return None + return cls + + def _finddoc(obj): + if inspect.isclass(obj): + for base in obj.__mro__: + if base is not object: + try: + doc = base.__doc__ + except AttributeError: + continue + if doc is not None: + return doc + return None + + if inspect.ismethod(obj) and getattr(obj, '__self__', None): + name = obj.__func__.__name__ + self = obj.__self__ + if (inspect.isclass(self) and + getattr(getattr(self, name, None), '__func__') + is obj.__func__): + # classmethod + cls = self + else: + cls = self.__class__ + elif inspect.isfunction(obj) or inspect.ismethod(obj): + name = obj.__name__ + cls = _findclass(obj) + if cls is None or getattr(cls, name) != obj: + return None + elif inspect.isbuiltin(obj): + name = obj.__name__ + self = obj.__self__ + if (inspect.isclass(self) and + self.__qualname__ + '.' + name == obj.__qualname__): + # classmethod + cls = self + else: + cls = self.__class__ + # Should be tested before isdatadescriptor(). + elif isinstance(obj, property): + func = obj.fget + name = func.__name__ + cls = _findclass(func) + if cls is None or getattr(cls, name) is not obj: + return None + elif inspect.ismethoddescriptor(obj) or inspect.isdatadescriptor(obj): + name = obj.__name__ + cls = obj.__objclass__ + if getattr(cls, name) is not obj: + return None + else: + return None + + for base in cls.__mro__: + try: + doc = getattr(base, name).__doc__ + except AttributeError: + continue + if doc is not None: + return doc + return None + + def getdoc(object): + """Get the documentation string for an object. + + All tabs are expanded to spaces. To clean up docstrings that are + indented to line up with blocks of code, any whitespace than can be + uniformly removed from the second line onwards is removed.""" + try: + doc = object.__doc__ + except AttributeError: + return None + if doc is None: + try: + doc = _finddoc(object) + except (AttributeError, TypeError): + return None + if not isinstance(doc, str): + return None + return inspect.cleandoc(doc) diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index caf31b7e9..af94de80e 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -431,6 +431,13 @@ def test_get_doc(): directive.env.config.autoclass_content = 'both' assert getdocl('class', I) == ['Class docstring', '', 'New docstring'] + # NOTE: inspect.getdoc seems not to work with locally defined classes + directive.env.config.autodoc_inherit_docstrings = False + assert getdocl('method', Base.inheritedmeth) == ['Inherited function.'] + assert getdocl('method', Derived.inheritedmeth) == [] + directive.env.config.autodoc_inherit_docstrings = True + assert getdocl('method', Derived.inheritedmeth) == ['Inherited function.'] + @pytest.mark.usefixtures('setup_test') def test_docstring_processing(): @@ -941,6 +948,12 @@ class Base(object): """Inherited function.""" +class Derived(Base): + def inheritedmeth(self): + # no docstring here + pass + + class Class(Base): """Class to document."""