Repository: sphinx-doc/sphinx

Issue:
No docs generated for functools.partial functions
Subject: No docs appear for functions created via functools.partial

<!--
  Important: This is a list of issues for Sphinx, not a forum.
  If you'd like to post a question, please move to sphinx-users group.
  https://groups.google.com/forum/#!forum/sphinx-users

  Thanks,
-->

### Problem
I have a module which includes some functions created using `functools.partial` and I would like to include those in my doc page. I've used `func.__doc__ = ...` to set the docstring for each partial function.
When I generate the docs the partial functions are completely ignored. I've created a toy example below to reproduce the problem.

#### Procedure to reproduce the problem

I created a directory with this structure:
```
- doc/
- example/__init__.py  #empty
- example/code.py
```
`example/code.py` contains:

```python
from functools import partial

def my_func(a, b, c):
    """my_func docstring

    Args:
        a (int): a
        b (int): b
        c (int): c

    Returns:
        int: a + b + c
    """
    return a + b + c

partial_func = partial(my_func, c=10)
partial_func.__doc__ = """partial_func docstring
    Args:
        a (int): a
        b (int): b

    Returns:
        int: a + b + 10
"""
```
I set up my python environment and `sphinx` using `conda` like this:
```
$ conda create -n test python=3
$ conda activate test
$ conda install sphinx
$ cd doc
$ sphinx-quickstart
> Separate source and build directories (y/n) [n]: y
> Name prefix for templates and static dir [_]: 
> Project name: Example
> Author name(s): Ioanna
> Project release []: 
> Project language [en]: 
> Source file suffix [.rst]: 
> Name of your master document (without suffix) [index]: 
> Do you want to use the epub builder (y/n) [n]: 
> autodoc: automatically insert docstrings from modules (y/n) [n]: y
> doctest: automatically test code snippets in doctest blocks (y/n) [n]: 
> intersphinx: link between Sphinx documentation of different projects (y/n) [n]: 
> todo: write "todo" entries that can be shown or hidden on build (y/n) [n]: 
> coverage: checks for documentation coverage (y/n) [n]: 
> imgmath: include math, rendered as PNG or SVG images (y/n) [n]: 
> mathjax: include math, rendered in the browser by MathJax (y/n) [n]: 
> ifconfig: conditional inclusion of content based on config values (y/n) [n]: 
> viewcode: include links to the source code of documented Python objects (y/n) [n]: 
> githubpages: create .nojekyll file to publish the document on GitHub pages (y/n) [n]: 
> Create Makefile? (y/n) [y]: 
> Create Windows command file? (y/n) [y]: n
```
I also modified the resulting `conf.py` to add 'sphinx.ext.napoleon' to the extensions being used to make sure it recognizes the Google style docstrings.

Then I added this to `index.rst`:

```
.. automodule:: example.code
    :members:
```

#### Error logs / results

Running `make html` gives the following output
```
Running Sphinx v1.7.5
loading pickled environment... done
building [mo]: targets for 0 po files that are out of date
building [html]: targets for 0 source files that are out of date
updating environment: 0 added, 1 changed, 0 removed
reading sources... [100%] index                                                                                                         
looking for now-outdated files... none found
pickling environment... done
checking consistency... done
preparing documents... done
writing output... [100%] index                                                                                                          
generating indices... genindex py-modindex
writing additional pages... search
copying static files... done
copying extra files... done
dumping search index in English (code: en) ... done
dumping object inventory... done
build succeeded.

The HTML pages are in build/html.
```
The resulting docs correctly display the docs for `my_func`, but `partial_func` is completely absent. It doesn't even appear as a function signature.

I tried setting ` :members: partial_func` in `index.rst` as well, but it still didn't appear.

### Environment info
- OS: Fedora 25
- Python version: 3.7.0
- Sphinx version: 1.7.5
- Here's the output of `conda list` on my test env:
```
alabaster                 0.7.11                   py37_0  
asn1crypto                0.24.0                   py37_0  
babel                     2.6.0                    py37_0  
ca-certificates           2018.03.07                    0  
certifi                   2018.4.16                py37_0  
cffi                      1.11.5           py37h9745a5d_0  
chardet                   3.0.4                    py37_1  
cryptography              2.2.2            py37h14c3975_0  
docutils                  0.14                     py37_0  
idna                      2.7                      py37_0  
imagesize                 1.0.0                    py37_0  
jinja2                    2.10                     py37_0  
libedit                   3.1.20170329         h6b74fdf_2  
libffi                    3.2.1                hd88cf55_4  
libgcc-ng                 7.2.0                hdf63c60_3  
libstdcxx-ng              7.2.0                hdf63c60_3  
markupsafe                1.0              py37h14c3975_1  
ncurses                   6.1                  hf484d3e_0  
openssl                   1.0.2o               h20670df_0  
packaging                 17.1                     py37_0  
pip                       10.0.1                   py37_0  
pycparser                 2.18                     py37_1  
pygments                  2.2.0                    py37_0  
pyopenssl                 18.0.0                   py37_0  
pyparsing                 2.2.0                    py37_1  
pysocks                   1.6.8                    py37_0  
python                    3.7.0                hc3d631a_0  
pytz                      2018.5                   py37_0  
readline                  7.0                  ha6073c6_4  
requests                  2.19.1                   py37_0  
setuptools                39.2.0                   py37_0  
six                       1.11.0                   py37_1  
snowballstemmer           1.2.1                    py37_0  
sphinx                    1.7.5                    py37_0  
sphinxcontrib             1.0                      py37_1  
sphinxcontrib-websupport  1.1.0                    py37_1  
sqlite                    3.24.0               h84994c4_0  
tk                        8.6.7                hc745277_3  
urllib3                   1.23                     py37_0  
wheel                     0.31.1                   py37_0  
xz                        5.2.4                h14c3975_4  
zlib                      1.2.11               ha838bed_2  
```


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/sphinx-doc/sphinx/pull/5305?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`1.7@01f4d3b`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `94.73%`.

[![Impacted file tree graph](https://codecov.io/gh/sphinx-doc/sphinx/pull/5305/graphs/tree.svg?width=650&height=150&src=pr&token=VFlFSELbkw)](https://codecov.io/gh/sphinx-doc/sphinx/pull/5305?src=pr&el=tree)

```diff
@@          Coverage Diff           @@
##             1.7    #5305   +/-   ##
======================================
  Coverage       ?   82.06%           
======================================
  Files          ?      289           
  Lines          ?    38238           
  Branches       ?     5931           
======================================
  Hits           ?    31379           
  Misses         ?     5538           
  Partials       ?     1321
```


| [Impacted Files](https://codecov.io/gh/sphinx-doc/sphinx/pull/5305?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [sphinx/util/inspect.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5305/diff?src=pr&el=tree#diff-c3BoaW54L3V0aWwvaW5zcGVjdC5weQ==) | `76.33% <100%> (ø)` | |
| [tests/test\_autodoc.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5305/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9hdXRvZG9jLnB5) | `96.35% <100%> (ø)` | |
| [sphinx/ext/autodoc/\_\_init\_\_.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5305/diff?src=pr&el=tree#diff-c3BoaW54L2V4dC9hdXRvZG9jL19faW5pdF9fLnB5) | `84.55% <83.33%> (ø)` | |
| [...s/roots/test-ext-autodoc/target/partialfunction.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5305/diff?src=pr&el=tree#diff-dGVzdHMvcm9vdHMvdGVzdC1leHQtYXV0b2RvYy90YXJnZXQvcGFydGlhbGZ1bmN0aW9uLnB5) | `83.33% <83.33%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/sphinx-doc/sphinx/pull/5305?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/sphinx-doc/sphinx/pull/5305?src=pr&el=footer). Last update [01f4d3b...8a98254](https://codecov.io/gh/sphinx-doc/sphinx/pull/5305?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/CHANGES b/CHANGES
index 58431cee8..e17247476 100644
--- a/CHANGES
+++ b/CHANGES
@@ -29,6 +29,7 @@ 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
 * #5298: imgmath: math_number_all causes equations to have two numbers in html
 
 Testing
diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py
index 058fd7765..7d34e185d 100644
--- a/sphinx/ext/autodoc/__init__.py
+++ b/sphinx/ext/autodoc/__init__.py
@@ -32,7 +32,7 @@
 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
@@ -399,7 +399,9 @@ def check_module(self):
             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
 
@@ -473,9 +475,8 @@ def add_directive_header(self, sig):
     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):
@@ -599,9 +600,7 @@ def filter_members(self, members, want_all):
             # 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
@@ -1022,12 +1021,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:
@@ -1095,7 +1093,7 @@ def format_args(self):
         # __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()
@@ -1304,8 +1302,7 @@ def import_object(self):
 
     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):
@@ -1336,7 +1333,7 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
 
     @staticmethod
     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
     def can_document_member(cls, member, membername, isattr, parent):
diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py
index 64e93f267..3b5d48e2a 100644
--- a/sphinx/util/inspect.py
+++ b/sphinx/util/inspect.py
@@ -15,6 +15,7 @@
 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
@@ -99,8 +100,6 @@ def getargspec(func):
                                    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."""
@@ -155,6 +154,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."""
@@ -198,6 +203,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."""
@@ -601,7 +618,7 @@ def format_annotation_old(self, annotation):
 
 
 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
@@ -679,7 +696,7 @@ def _finddoc(obj):
                 return doc
         return None
 
-    def getdoc(object):
+    def _getdoc(object):
         """Get the documentation string for an object.
 
         All tabs are expanded to spaces.  To clean up docstrings that are
@@ -697,3 +714,21 @@ def getdoc(object):
         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


Test patch:
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 53d8a1e14..97e03c886 100644
--- a/tests/test_autodoc.py
+++ b/tests/test_autodoc.py
@@ -912,6 +912,44 @@ def assert_order(items, objtype, name, member_order, **kw):
                            '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),
                     reason='functools.partialmethod is available on py34 or above')
 @pytest.mark.usefixtures('setup_test')