diff --git a/CHANGES b/CHANGES index d9b28c7d3..60b3354f9 100644 --- a/CHANGES +++ b/CHANGES @@ -264,6 +264,8 @@ Bugs fixed * #5280: autodoc: Fix wrong type annotations for complex typing * autodoc: Optional types are wrongly rendered * #5291: autodoc crashed by ForwardRef types +* #5211: autodoc: No docs generated for functools.partial functions +* #5306: autodoc: ``getargspec()`` raises NameError for invalid typehints * #5298: imgmath: math_number_all causes equations to have two numbers in html Testing diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 6a44e16d5..772a1c989 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -333,8 +333,9 @@ class BuildEnvironment(object): self.all_docs[docname] = other.all_docs[docname] if docname in other.reread_always: self.reread_always.add(docname) - if docname in other.included: - self.included.add(docname) + + for docname in other.included: + self.included.add(docname) for domainname, domain in self.domains.items(): domain.merge_domaindata(docnames, other.domaindata[domainname]) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 6671df579..4b585160a 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -33,7 +33,7 @@ from sphinx.util import rpartition, force_decode from sphinx.util.docstrings import prepare_docstring from sphinx.util.inspect import Signature, isdescriptor, safe_getmembers, \ safe_getattr, object_description, is_builtin_class_method, \ - isenumattribute, isclassmethod, isstaticmethod, getdoc + isenumattribute, isclassmethod, isstaticmethod, isfunction, isbuiltin, ispartial, getdoc if False: # For type annotation @@ -401,7 +401,9 @@ class Documenter(object): return True 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 True @@ -475,9 +477,8 @@ class Documenter(object): def get_doc(self, encoding=None, ignore=1): # 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) + docstring = getdoc(self.object, self.get_attr, + self.env.config.autodoc_inherit_docstrings) # make sure we have Unicode docstrings, then sanitize and split # into lines if isinstance(docstring, text_type): @@ -601,9 +602,7 @@ class Documenter(object): # if isattr is True, the member is documented as an attribute isattr = False - doc = self.get_attr(member, '__doc__', None) - if doc is None and self.env.config.autodoc_inherit_docstrings: - doc = getdoc(member) + doc = getdoc(member, self.get_attr, self.env.config.autodoc_inherit_docstrings) # if the member __doc__ is the same as self's __doc__, it's just # inherited and therefore not the member's doc @@ -1029,12 +1028,11 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ @classmethod def can_document_member(cls, member, membername, isattr, parent): # type: (Any, unicode, bool, Any) -> bool - return inspect.isfunction(member) or inspect.isbuiltin(member) + return isfunction(member) or isbuiltin(member) def format_args(self): # type: () -> unicode - if inspect.isbuiltin(self.object) or \ - inspect.ismethoddescriptor(self.object): + if isbuiltin(self.object) or inspect.ismethoddescriptor(self.object): # cannot introspect arguments of a C function or method return None try: @@ -1102,7 +1100,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: # __init__ written in C? if initmeth is None 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 try: return Signature(initmeth, bound_method=True, has_retval=False).format_args() @@ -1317,8 +1315,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: def format_args(self): # type: () -> unicode - if inspect.isbuiltin(self.object) or \ - inspect.ismethoddescriptor(self.object): + if isbuiltin(self.object) or inspect.ismethoddescriptor(self.object): # can never get arguments of a C function or method return None if isstaticmethod(self.object, cls=self.parent, name=self.object_name): @@ -1350,7 +1347,7 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): @staticmethod def is_function_or_method(obj): # type: (Any) -> bool - return inspect.isfunction(obj) or inspect.isbuiltin(obj) or inspect.ismethod(obj) + return isfunction(obj) or isbuiltin(obj) or inspect.ismethod(obj) @classmethod def can_document_member(cls, member, membername, isattr, parent): diff --git a/sphinx/ext/autodoc/inspector.py b/sphinx/ext/autodoc/inspector.py index 6e07c9547..be42237c6 100644 --- a/sphinx/ext/autodoc/inspector.py +++ b/sphinx/ext/autodoc/inspector.py @@ -130,8 +130,11 @@ def formatargspec(function, args, varargs=None, varkw=None, defaults=None, else: return value - introspected_hints = (typing.get_type_hints(function) # type: ignore - if typing and hasattr(function, '__code__') else {}) + try: + introspected_hints = (typing.get_type_hints(function) # type: ignore + if typing and hasattr(function, '__code__') else {}) + except Exception: + introspected_hints = {} fd = StringIO() fd.write('(') diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 487b95f1f..1512a9918 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -15,6 +15,7 @@ import re import sys import typing from collections import OrderedDict +from functools import partial from six import PY2, PY3, StringIO, binary_type, string_types, itervalues from six.moves import builtins @@ -102,8 +103,6 @@ if PY3: kwonlyargs, kwdefaults, annotations) else: # 2.7 - from functools import partial - def getargspec(func): # type: (Any) -> Any """Like inspect.getargspec but supports functools.partial as well.""" @@ -158,6 +157,12 @@ def isenumattribute(x): return isinstance(x, enum.Enum) +def ispartial(obj): + # type: (Any) -> bool + """Check if the object is partial.""" + return isinstance(obj, partial) + + def isclassmethod(obj): # type: (Any) -> bool """Check if the object is classmethod.""" @@ -201,6 +206,18 @@ def isdescriptor(x): 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): # type: (Any, unicode, unicode) -> object """A getattr() that turns all exceptions into AttributeErrors.""" @@ -616,7 +633,7 @@ class Signature(object): if sys.version_info >= (3, 5): - getdoc = inspect.getdoc + _getdoc = inspect.getdoc else: # code copied from the inspect.py module of the standard library # of Python 3.5 @@ -696,7 +713,7 @@ else: return doc return None - def getdoc(object): + def _getdoc(object): # type: (Any) -> unicode """Get the documentation string for an object. @@ -715,3 +732,21 @@ else: if not isinstance(doc, str): return None 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 diff --git a/tests/roots/test-ext-autodoc/target/partialfunction.py b/tests/roots/test-ext-autodoc/target/partialfunction.py new file mode 100644 index 000000000..727a62680 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/partialfunction.py @@ -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" diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 0cd543ad7..767ab418e 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -1374,6 +1374,44 @@ def test_mocked_module_imports(app): ] +@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), reason='functools.partialmethod is available on py34 or above') @pytest.mark.sphinx('html', testroot='ext-autodoc')