Merge pull request #5305 from tk0miya/5211_autodoc_for_partial_functions

Fix #5211: autodoc: No docs generated for functools.partial functions
This commit is contained in:
Takeshi KOMIYA 2018-08-17 02:06:20 +09:00 committed by GitHub
commit 2604920232
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 101 additions and 19 deletions

View File

@ -29,6 +29,7 @@ Bugs fixed
* #5280: autodoc: Fix wrong type annotations for complex typing * #5280: autodoc: Fix wrong type annotations for complex typing
* autodoc: Optional types are wrongly rendered * autodoc: Optional types are wrongly rendered
* #5291: autodoc crashed by ForwardRef types * #5291: autodoc crashed by ForwardRef types
* #5211: autodoc: No docs generated for functools.partial functions
* #5298: imgmath: math_number_all causes equations to have two numbers in html * #5298: imgmath: math_number_all causes equations to have two numbers in html
Testing Testing

View File

@ -32,7 +32,7 @@ from sphinx.util import rpartition, force_decode
from sphinx.util.docstrings import prepare_docstring from sphinx.util.docstrings import prepare_docstring
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, \
isenumattribute, isclassmethod, isstaticmethod, getdoc isenumattribute, isclassmethod, isstaticmethod, isfunction, isbuiltin, ispartial, getdoc
if False: if False:
# For type annotation # For type annotation
@ -399,7 +399,9 @@ class Documenter(object):
return True return True
modname = self.get_attr(self.object, '__module__', None) modname = self.get_attr(self.object, '__module__', None)
if modname and modname != self.modname: if ispartial(self.object) and modname == '_functools': # for pypy
return True
elif modname and modname != self.modname:
return False return False
return True return True
@ -473,9 +475,8 @@ class Documenter(object):
def get_doc(self, encoding=None, ignore=1): def get_doc(self, encoding=None, ignore=1):
# 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 = getdoc(self.object, self.get_attr,
if docstring is None and self.env.config.autodoc_inherit_docstrings: 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):
@ -599,9 +600,7 @@ class Documenter(object):
# if isattr is True, the member is documented as an attribute # if isattr is True, the member is documented as an attribute
isattr = False isattr = False
doc = self.get_attr(member, '__doc__', None) doc = getdoc(member, self.get_attr, self.env.config.autodoc_inherit_docstrings)
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
@ -1022,12 +1021,11 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
@classmethod @classmethod
def can_document_member(cls, member, membername, isattr, parent): def can_document_member(cls, member, membername, isattr, parent):
# type: (Any, unicode, bool, Any) -> bool # type: (Any, unicode, bool, Any) -> bool
return inspect.isfunction(member) or inspect.isbuiltin(member) return isfunction(member) or isbuiltin(member)
def format_args(self): def format_args(self):
# type: () -> unicode # type: () -> unicode
if inspect.isbuiltin(self.object) or \ if isbuiltin(self.object) or inspect.ismethoddescriptor(self.object):
inspect.ismethoddescriptor(self.object):
# cannot introspect arguments of a C function or method # cannot introspect arguments of a C function or method
return None return None
try: try:
@ -1095,7 +1093,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
# __init__ written in C? # __init__ written in C?
if initmeth is None or \ if initmeth is None or \
is_builtin_class_method(self.object, '__init__') or \ is_builtin_class_method(self.object, '__init__') or \
not(inspect.ismethod(initmeth) or inspect.isfunction(initmeth)): not(inspect.ismethod(initmeth) or isfunction(initmeth)):
return None return None
try: try:
return Signature(initmeth, bound_method=True, has_retval=False).format_args() return Signature(initmeth, bound_method=True, has_retval=False).format_args()
@ -1304,8 +1302,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
def format_args(self): def format_args(self):
# type: () -> unicode # type: () -> unicode
if inspect.isbuiltin(self.object) or \ if isbuiltin(self.object) or inspect.ismethoddescriptor(self.object):
inspect.ismethoddescriptor(self.object):
# can never get arguments of a C function or method # can never get arguments of a C function or method
return None return None
if isstaticmethod(self.object, cls=self.parent, name=self.object_name): if isstaticmethod(self.object, cls=self.parent, name=self.object_name):
@ -1336,7 +1333,7 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
@staticmethod @staticmethod
def is_function_or_method(obj): def is_function_or_method(obj):
return inspect.isfunction(obj) or inspect.isbuiltin(obj) or inspect.ismethod(obj) return isfunction(obj) or isbuiltin(obj) or inspect.ismethod(obj)
@classmethod @classmethod
def can_document_member(cls, member, membername, isattr, parent): def can_document_member(cls, member, membername, isattr, parent):

View File

@ -15,6 +15,7 @@ import re
import sys import sys
import typing import typing
from collections import OrderedDict from collections import OrderedDict
from functools import partial
from six import PY2, PY3, StringIO, binary_type, string_types, itervalues from six import PY2, PY3, StringIO, binary_type, string_types, itervalues
from six.moves import builtins from six.moves import builtins
@ -99,8 +100,6 @@ if PY3:
kwonlyargs, kwdefaults, annotations) kwonlyargs, kwdefaults, annotations)
else: # 2.7 else: # 2.7
from functools import partial
def getargspec(func): def getargspec(func):
# type: (Any) -> Any # type: (Any) -> Any
"""Like inspect.getargspec but supports functools.partial as well.""" """Like inspect.getargspec but supports functools.partial as well."""
@ -155,6 +154,12 @@ def isenumattribute(x):
return isinstance(x, enum.Enum) return isinstance(x, enum.Enum)
def ispartial(obj):
# type: (Any) -> bool
"""Check if the object is partial."""
return isinstance(obj, partial)
def isclassmethod(obj): def isclassmethod(obj):
# type: (Any) -> bool # type: (Any) -> bool
"""Check if the object is classmethod.""" """Check if the object is classmethod."""
@ -198,6 +203,18 @@ def isdescriptor(x):
return False return False
def isfunction(obj):
# type: (Any) -> bool
"""Check if the object is function."""
return inspect.isfunction(obj) or ispartial(obj) and inspect.isfunction(obj.func)
def isbuiltin(obj):
# type: (Any) -> bool
"""Check if the object is builtin."""
return inspect.isbuiltin(obj) or ispartial(obj) and inspect.isbuiltin(obj.func)
def safe_getattr(obj, name, *defargs): def safe_getattr(obj, name, *defargs):
# type: (Any, unicode, unicode) -> object # type: (Any, unicode, unicode) -> object
"""A getattr() that turns all exceptions into AttributeErrors.""" """A getattr() that turns all exceptions into AttributeErrors."""
@ -601,7 +618,7 @@ class Signature(object):
if sys.version_info >= (3, 5): if sys.version_info >= (3, 5):
getdoc = inspect.getdoc _getdoc = inspect.getdoc
else: else:
# code copied from the inspect.py module of the standard library # code copied from the inspect.py module of the standard library
# of Python 3.5 # of Python 3.5
@ -679,7 +696,7 @@ else:
return doc return doc
return None return None
def getdoc(object): def _getdoc(object):
"""Get the documentation string for an object. """Get the documentation string for an object.
All tabs are expanded to spaces. To clean up docstrings that are All tabs are expanded to spaces. To clean up docstrings that are
@ -697,3 +714,21 @@ else:
if not isinstance(doc, str): if not isinstance(doc, str):
return None return None
return inspect.cleandoc(doc) return inspect.cleandoc(doc)
def getdoc(obj, attrgetter=safe_getattr, allow_inherited=False):
# type: (Any, Callable, bool) -> unicode
"""Get the docstring for the object.
This tries to obtain the docstring for some kind of objects additionally:
* partial functions
* inherited docstring
"""
doc = attrgetter(obj, '__doc__', None)
if ispartial(obj) and doc == obj.__class__.__doc__:
return getdoc(obj.func)
elif doc is None and allow_inherited:
doc = _getdoc(obj)
return doc

View File

@ -0,0 +1,11 @@
from functools import partial
def func1():
"""docstring of func1"""
pass
func2 = partial(func1)
func3 = partial(func1)
func3.__doc__ = "docstring of func3"

View File

@ -912,6 +912,44 @@ def test_generate():
'module', 'autodoc_missing_imports') 'module', 'autodoc_missing_imports')
@pytest.mark.usefixtures('setup_test')
def test_partialfunction():
def call_autodoc(objtype, name):
inst = app.registry.documenters[objtype](directive, name)
inst.generate()
result = list(directive.result)
del directive.result[:]
return result
options.members = ALL
#options.undoc_members = True
expected = [
'',
'.. py:module:: target.partialfunction',
'',
'',
'.. py:function:: func1()',
' :module: target.partialfunction',
'',
' docstring of func1',
' ',
'',
'.. py:function:: func2()',
' :module: target.partialfunction',
'',
' docstring of func1',
' ',
'',
'.. py:function:: func3()',
' :module: target.partialfunction',
'',
' docstring of func3',
' '
]
assert call_autodoc('module', 'target.partialfunction') == expected
@pytest.mark.skipif(sys.version_info < (3, 4), @pytest.mark.skipif(sys.version_info < (3, 4),
reason='functools.partialmethod is available on py34 or above') reason='functools.partialmethod is available on py34 or above')
@pytest.mark.usefixtures('setup_test') @pytest.mark.usefixtures('setup_test')