Repository: sphinx-doc/sphinx

Issue:
Unable to document bound instance methods exported as module functions
**Is your feature request related to a problem? Please describe.**
We're building an API documentation site for Horovod project (http://horovod.ai) and faced the problem with documenting bound methods that we export as module functions (duck typing yay!)

Simple example:
```python
class _Boo(object):
    def m(self):
        """Everybody needs a method that returns 42, right?"""
        return 42


_b = _Boo()
n = _b.m
```

Real example: [declaration](https://github.com/horovod/horovod/blob/68f525c64cdd5a31536f5485448960fbb00d19fc/horovod/common/__init__.py#L58), [usage](https://github.com/horovod/horovod/blob/6643d6aa4abd583e5d254e2658e413a309f47a54/horovod/tensorflow/mpi_ops.py#L64).

I found that it does not show up in the docs because it does not match either of these conditions:

`FunctionDocumenter`:
```python
@classmethod
def can_document_member(cls, member, membername, isattr, parent):
    # type: (Any, unicode, bool, Any) -> bool
    return isfunction(member) or isbuiltin(member)
```

`MethodDocumenter`:
```python
@classmethod
def can_document_member(cls, member, membername, isattr, parent):
    # type: (Any, unicode, bool, Any) -> bool
    return inspect.isroutine(member) and \
        not isinstance(parent, ModuleDocumenter)
```

**Describe the solution you'd like**
I'd like those doc strings to show up in the docs.

I tried modifying both `MethodDocumenter` and `FunctionDocumenter` conditions to allow for `inspect.isroutine(member) and isinstance(parent, ModuleDocumenter)` and found that `FunctionDocumenter` gives more satisfying results.

What is the best way to document those functions?  Would you take a PR where `FunctionDocumenter` is modified as follows:

```python
@classmethod
def can_document_member(cls, member, membername, isattr, parent):
    # type: (Any, unicode, bool, Any) -> bool
    return isfunction(member) or isbuiltin(member) or (inspect.isroutine(member) and  isinstance(parent, ModuleDocumenter))
```

... or maybe there's another way to achieve this?

Discussion:
alsrgv: @tk0miya, please suggest additional tests, if any.
codecov[bot]: # [Codecov](https://codecov.io/gh/sphinx-doc/sphinx/pull/6288?src=pr&el=h1) Report
> Merging [#6288](https://codecov.io/gh/sphinx-doc/sphinx/pull/6288?src=pr&el=desc) into [master](https://codecov.io/gh/sphinx-doc/sphinx/commit/f4a98d8ca0cc88a5a993a6b90a3af0892fdc19c0?src=pr&el=desc) will **increase** coverage by `0.01%`.
> The diff coverage is `100%`.

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

```diff
@@            Coverage Diff             @@
##           master    #6288      +/-   ##
==========================================
+ Coverage   84.16%   84.18%   +0.01%     
==========================================
  Files         272      271       -1     
  Lines       39004    38988      -16     
  Branches     5808     5805       -3     
==========================================
- Hits        32829    32821       -8     
+ Misses       4859     4856       -3     
+ Partials     1316     1311       -5
```


| [Impacted Files](https://codecov.io/gh/sphinx-doc/sphinx/pull/6288?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [tests/test\_autodoc.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/6288/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9hdXRvZG9jLnB5) | `96.71% <100%> (+0.04%)` | :arrow_up: |
| [sphinx/ext/autodoc/\_\_init\_\_.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/6288/diff?src=pr&el=tree#diff-c3BoaW54L2V4dC9hdXRvZG9jL19faW5pdF9fLnB5) | `88.52% <100%> (ø)` | :arrow_up: |
| [sphinx/\_\_init\_\_.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/6288/diff?src=pr&el=tree#diff-c3BoaW54L19faW5pdF9fLnB5) | | |
| [sphinx/transforms/post\_transforms/images.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/6288/diff?src=pr&el=tree#diff-c3BoaW54L3RyYW5zZm9ybXMvcG9zdF90cmFuc2Zvcm1zL2ltYWdlcy5weQ==) | `66.01% <0%> (+1.3%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/sphinx-doc/sphinx/pull/6288?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/6288?src=pr&el=footer). Last update [f4a98d8...096029c](https://codecov.io/gh/sphinx-doc/sphinx/pull/6288?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py
index 4c1032db5..392c4e3b4 100644
--- a/sphinx/ext/autodoc/__init__.py
+++ b/sphinx/ext/autodoc/__init__.py
@@ -993,7 +993,9 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter):  # typ
     @classmethod
     def can_document_member(cls, member, membername, isattr, parent):
         # type: (Any, str, bool, Any) -> bool
-        return inspect.isfunction(member) or inspect.isbuiltin(member)
+        # supports functions, builtins and bound methods exported at the module level
+        return (inspect.isfunction(member) or inspect.isbuiltin(member) or
+                (inspect.isroutine(member) and isinstance(parent, ModuleDocumenter)))
 
     def format_args(self):
         # type: () -> str


Test patch:
diff --git a/tests/roots/test-ext-autodoc/target/bound_method.py b/tests/roots/test-ext-autodoc/target/bound_method.py
new file mode 100644
index 000000000..d48b9ee1c
--- /dev/null
+++ b/tests/roots/test-ext-autodoc/target/bound_method.py
@@ -0,0 +1,7 @@
+class Cls:
+    def method(self):
+        """Method docstring"""
+        pass
+
+
+bound_method = Cls().method
diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py
index 27412a9da..07e82b54a 100644
--- a/tests/test_autodoc.py
+++ b/tests/test_autodoc.py
@@ -259,6 +259,11 @@ def foo3(self, d='\n'):
     assert formatsig('method', 'H.foo', H.foo2, None, None) == '(*c)'
     assert formatsig('method', 'H.foo', H.foo3, None, None) == r"(d='\\n')"
 
+    # test bound methods interpreted as functions
+    assert formatsig('function', 'foo', H().foo1, None, None) == '(b, *c)'
+    assert formatsig('function', 'foo', H().foo2, None, None) == '(*c)'
+    assert formatsig('function', 'foo', H().foo3, None, None) == r"(d='\\n')"
+
     # test exception handling (exception is caught and args is '')
     directive.env.config.autodoc_docstring_signature = False
     assert formatsig('function', 'int', int, None, None) == ''
@@ -451,6 +456,14 @@ def __new__(cls):
     directive.env.config.autoclass_content = 'both'
     assert getdocl('class', I) == ['Class docstring', '', 'New docstring']
 
+    # verify that method docstrings get extracted in both normal case
+    # and in case of bound method posing as a function
+    class J:  # NOQA
+        def foo(self):
+            """Method docstring"""
+    assert getdocl('method', J.foo) == ['Method docstring']
+    assert getdocl('function', J().foo) == ['Method docstring']
+
     from target import Base, Derived
 
     # NOTE: inspect.getdoc seems not to work with locally defined classes
@@ -1463,6 +1476,23 @@ def test_partialfunction():
     ]
 
 
+@pytest.mark.usefixtures('setup_test')
+def test_bound_method():
+    options = {"members": None}
+    actual = do_autodoc(app, 'module', 'target.bound_method', options)
+    assert list(actual) == [
+        '',
+        '.. py:module:: target.bound_method',
+        '',
+        '',
+        '.. py:function:: bound_method()',
+        '   :module: target.bound_method',
+        '',
+        '   Method docstring',
+        '   ',
+    ]
+
+
 @pytest.mark.usefixtures('setup_test')
 def test_coroutine():
     options = {"members": None}