diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 3eeec4523..5b260c6fe 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 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 test_format_signature(): 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 test_get_doc(): 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}