Merge pull request #4012 from avalentino/docstring-inheritance

Docstring inheritance
This commit is contained in:
Takeshi KOMIYA 2017-08-21 23:31:17 +09:00 committed by GitHub
commit 8b76d5b064
6 changed files with 132 additions and 2 deletions

View File

@ -57,7 +57,7 @@ Other contributors, listed alphabetically, are:
* Stefan Seefeld -- toctree improvements * Stefan Seefeld -- toctree improvements
* Gregory Szorc -- performance improvements * Gregory Szorc -- performance improvements
* Taku Shimizu -- epub3 builder * Taku Shimizu -- epub3 builder
* Antonio Valentino -- qthelp builder * Antonio Valentino -- qthelp builder, docstring inheritance
* Filip Vavera -- napoleon todo directive * Filip Vavera -- napoleon todo directive
* Pauli Virtanen -- autodoc improvements, autosummary extension * Pauli Virtanen -- autodoc improvements, autosummary extension
* Stefan van der Walt -- autosummary extension * Stefan van der Walt -- autosummary extension

View File

@ -28,6 +28,7 @@ Features added
- ``ref.python`` (ref: #3866) - ``ref.python`` (ref: #3866)
* #3872: Add latex key to configure literal blocks caption position in PDF * #3872: Add latex key to configure literal blocks caption position in PDF
output (refs #3792, #1723) output (refs #3792, #1723)
* In case of missing docstring try to retrieve doc from base classes (ref: #3140)
Features removed Features removed

View File

@ -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 If ``False`` is given, autodoc forcely suppresses the error if the imported
module emits warnings. By default, ``True``. 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 Docstring preprocessing
----------------------- -----------------------

View File

@ -35,7 +35,7 @@ from sphinx.util import logging
from sphinx.util.nodes import nested_parse_with_titles from sphinx.util.nodes import nested_parse_with_titles
from sphinx.util.inspect import Signature, isdescriptor, safe_getmembers, \ from sphinx.util.inspect import Signature, isdescriptor, safe_getmembers, \
safe_getattr, object_description, is_builtin_class_method, \ safe_getattr, object_description, is_builtin_class_method, \
isenumclass, isenumattribute isenumclass, isenumattribute, getdoc
from sphinx.util.docstrings import prepare_docstring from sphinx.util.docstrings import prepare_docstring
if False: if False:
@ -525,6 +525,8 @@ class Documenter(object):
# type: (unicode, int) -> List[List[unicode]] # type: (unicode, int) -> List[List[unicode]]
"""Decode and return lines of the docstring(s) for the object.""" """Decode and return lines of the docstring(s) for the object."""
docstring = self.get_attr(self.object, '__doc__', None) 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 # make sure we have Unicode docstrings, then sanitize and split
# into lines # into lines
if isinstance(docstring, text_type): if isinstance(docstring, text_type):
@ -682,6 +684,9 @@ class Documenter(object):
isattr = False isattr = False
doc = self.get_attr(member, '__doc__', None) 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 # if the member __doc__ is the same as self's __doc__, it's just
# inherited and therefore not the member's doc # inherited and therefore not the member's doc
cls = self.get_attr(member, '__class__', None) 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_docstring_signature', True, True)
app.add_config_value('autodoc_mock_imports', [], True) app.add_config_value('autodoc_mock_imports', [], True)
app.add_config_value('autodoc_warningiserror', True, 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-docstring')
app.add_event('autodoc-process-signature') app.add_event('autodoc-process-signature')
app.add_event('autodoc-skip-member') app.add_event('autodoc-skip-member')

View File

@ -11,6 +11,7 @@
from __future__ import absolute_import from __future__ import absolute_import
import re import re
import sys
import typing import typing
import inspect import inspect
from collections import OrderedDict from collections import OrderedDict
@ -456,3 +457,102 @@ class Signature(object):
', '.join(param_strings)) ', '.join(param_strings))
return qualified_name 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)

View File

@ -431,6 +431,13 @@ def test_get_doc():
directive.env.config.autoclass_content = 'both' directive.env.config.autoclass_content = 'both'
assert getdocl('class', I) == ['Class docstring', '', 'New docstring'] 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') @pytest.mark.usefixtures('setup_test')
def test_docstring_processing(): def test_docstring_processing():
@ -941,6 +948,12 @@ class Base(object):
"""Inherited function.""" """Inherited function."""
class Derived(Base):
def inheritedmeth(self):
# no docstring here
pass
class Class(Base): class Class(Base):
"""Class to document.""" """Class to document."""