From e0abb107929df0d2e97daa7cddeda1e7b4763d60 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 13 Apr 2019 20:14:09 +0900 Subject: [PATCH 1/4] Fix #4777: Add :async: option to py:function and py:method directives --- CHANGES | 5 ++-- doc/usage/restructuredtext/domains.rst | 12 +++++++++- sphinx/domains/python.py | 25 ++++++++++++++++--- tests/test_domain_py.py | 33 ++++++++++++++++++++++---- 4 files changed, 64 insertions(+), 11 deletions(-) diff --git a/CHANGES b/CHANGES index e07b67059..013b0733f 100644 --- a/CHANGES +++ b/CHANGES @@ -78,8 +78,9 @@ Features added * #6212 autosummary: Add :confval:`autosummary_imported_members` to display imported members on autosummary * #6271: ``make clean`` is catastrophically broken if building into '.' -* Add ``:classmethod:`` and ``:staticmethod:`` options to :rst:dir:`py:method` - directive +* #4777: py domain: Add ``:async:`` option to :rst:dir:`py:function` directive +* py domain: Add ``:async:``, ``:classmethod:`` and ``:staticmethod:`` options + to :rst:dir:`py:method` directive Bugs fixed ---------- diff --git a/doc/usage/restructuredtext/domains.rst b/doc/usage/restructuredtext/domains.rst index 10dc93a07..10fbf6f6f 100644 --- a/doc/usage/restructuredtext/domains.rst +++ b/doc/usage/restructuredtext/domains.rst @@ -169,6 +169,13 @@ The following directives are provided for module and class contents: This information can (in any ``py`` directive) optionally be given in a structured form, see :ref:`info-field-lists`. + The ``async`` option can be given (with no value) to indicate the function is + an async method. + + .. versionchanged:: 2.1 + + ``:async:`` option added. + .. rst:directive:: .. py:data:: name Describes global data in a module, including both variables and values used @@ -216,12 +223,15 @@ The following directives are provided for module and class contents: described for ``function``. See also :ref:`signatures` and :ref:`info-field-lists`. + The ``async`` option can be given (with no value) to indicate the method is + an async method. + The ``classmethod`` option and ``staticmethod`` option can be given (with no value) to indicate the method is a class method (or a static method). .. versionchanged:: 2.1 - ``:classmethod:`` and ``:staticmethod:`` options added. + ``:async:``, ``:classmethod:`` and ``:staticmethod:`` options added. .. rst:directive:: .. py:staticmethod:: name(parameters) diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index e268023a5..c1ef3f990 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -438,6 +438,18 @@ class PyModulelevel(PyObject): class PyFunction(PyObject): """Description of a function.""" + option_spec = PyObject.option_spec.copy() + option_spec.update({ + 'async': directives.flag, + }) + + def get_signature_prefix(self, sig): + # type: (str) -> str + if 'async' in self.options: + return 'async ' + else: + return '' + def needs_arglist(self): # type: () -> bool return True @@ -573,6 +585,7 @@ class PyMethod(PyObject): option_spec = PyObject.option_spec.copy() option_spec.update({ + 'async': directives.flag, 'classmethod': directives.flag, 'staticmethod': directives.flag, }) @@ -583,10 +596,16 @@ class PyMethod(PyObject): def get_signature_prefix(self, sig): # type: (str) -> str + prefix = [] + if 'async' in self.options: + prefix.append('async') if 'staticmethod' in self.options: - return 'static ' - elif 'classmethod' in self.options: - return 'classmethod ' + prefix.append('static') + if 'classmethod' in self.options: + prefix.append('classmethod') + + if prefix: + return ' '.join(prefix) + ' ' else: return '' diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 5a4db3299..d3c685388 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -304,15 +304,24 @@ def test_pydata(app): def test_pyfunction(app): - text = ".. py:function:: func\n" + text = (".. py:function:: func1\n" + ".. py:function:: func2\n" + " :async:\n") domain = app.env.get_domain('py') doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_name, "func"], + [desc, ([desc_signature, ([desc_name, "func1"], + [desc_parameterlist, ()])], + [desc_content, ()])], + addnodes.index, + [desc, ([desc_signature, ([desc_annotation, "async "], + [desc_name, "func2"], [desc_parameterlist, ()])], [desc_content, ()])])) - assert 'func' in domain.objects - assert domain.objects['func'] == ('index', 'function') + assert 'func1' in domain.objects + assert domain.objects['func1'] == ('index', 'function') + assert 'func2' in domain.objects + assert domain.objects['func2'] == ('index', 'function') def test_pymethod_options(app): @@ -322,7 +331,9 @@ def test_pymethod_options(app): " .. py:method:: meth2\n" " :classmethod:\n" " .. py:method:: meth3\n" - " :staticmethod:\n") + " :staticmethod:\n" + " .. py:method:: meth4\n" + " :async:\n") domain = app.env.get_domain('py') doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, @@ -333,6 +344,8 @@ def test_pymethod_options(app): addnodes.index, desc, addnodes.index, + desc, + addnodes.index, desc)])])) # method @@ -364,6 +377,16 @@ def test_pymethod_options(app): assert 'Class.meth3' in domain.objects assert domain.objects['Class.meth3'] == ('index', 'method') + # :async: + assert_node(doctree[1][1][6], addnodes.index, + entries=[('single', 'meth4() (Class method)', 'Class.meth4', '', None)]) + assert_node(doctree[1][1][7], ([desc_signature, ([desc_annotation, "async "], + [desc_name, "meth4"], + [desc_parameterlist, ()])], + [desc_content, ()])) + assert 'Class.meth4' in domain.objects + assert domain.objects['Class.meth4'] == ('index', 'method') + def test_pyclassmethod(app): text = (".. py:class:: Class\n" From a77613fcfaf1323a46ba5645cc5569da7457d2fa Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 13 Apr 2019 23:08:52 +0900 Subject: [PATCH 2/4] pycode: Support "async" syntax --- sphinx/pycode/parser.py | 4 ++++ tests/test_pycode_parser.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index bf80f4367..9f9f7dd29 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -381,6 +381,10 @@ class VariableCommentPicker(ast.NodeVisitor): self.context.pop() self.current_function = None + def visit_AsyncFunctionDef(self, node): + # type: (ast.AsyncFunctionDef) -> None + self.visit_FunctionDef(node) # type: ignore + class DefinitionFinder(TokenProcessor): def __init__(self, lines): diff --git a/tests/test_pycode_parser.py b/tests/test_pycode_parser.py index 403c918dc..ba9778b80 100644 --- a/tests/test_pycode_parser.py +++ b/tests/test_pycode_parser.py @@ -314,6 +314,21 @@ def test_decorators(): 'Foo.method': ('def', 13, 15)} +def test_async_function_and_method(): + source = ('async def some_function():\n' + ' """docstring"""\n' + ' a = 1 + 1 #: comment1\n' + '\n' + 'class Foo:\n' + ' async def method(self):\n' + ' pass\n') + parser = Parser(source) + parser.parse() + assert parser.definitions == {'some_function': ('def', 1, 3), + 'Foo': ('class', 5, 7), + 'Foo.method': ('def', 6, 7)} + + def test_formfeed_char(): source = ('class Foo:\n' '\f\n' From a765c2e4ab13e10dd613711a8da93940fd124d43 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 13 Apr 2019 23:01:55 +0900 Subject: [PATCH 3/4] Add sphinx.util.inspect:iscoroutinefunction() --- sphinx/util/inspect.py | 16 ++++++++++++++-- tests/roots/test-ext-autodoc/target/functions.py | 4 ++++ tests/roots/test-ext-autodoc/target/methods.py | 5 +++++ tests/test_util_inspect.py | 16 ++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 877f727d4..a05110496 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -15,7 +15,7 @@ import re import sys import typing import warnings -from functools import partial +from functools import partial, partialmethod from inspect import ( # NOQA isclass, ismethod, ismethoddescriptor, isroutine ) @@ -129,7 +129,7 @@ def isenumattribute(x): def ispartial(obj): # type: (Any) -> bool """Check if the object is partial.""" - return isinstance(obj, partial) + return isinstance(obj, (partial, partialmethod)) def isclassmethod(obj): @@ -212,6 +212,18 @@ def isbuiltin(obj): return inspect.isbuiltin(obj) or ispartial(obj) and inspect.isbuiltin(obj.func) +def iscoroutinefunction(obj): + # type: (Any) -> bool + """Check if the object is coroutine-function.""" + if inspect.iscoroutinefunction(obj): + return True + elif ispartial(obj) and inspect.iscoroutinefunction(obj.func): + # partialed + return True + else: + return False + + def safe_getattr(obj, name, *defargs): # type: (Any, str, str) -> object """A getattr() that turns all exceptions into AttributeErrors.""" diff --git a/tests/roots/test-ext-autodoc/target/functions.py b/tests/roots/test-ext-autodoc/target/functions.py index 7c79188d9..8ff00f734 100644 --- a/tests/roots/test-ext-autodoc/target/functions.py +++ b/tests/roots/test-ext-autodoc/target/functions.py @@ -5,7 +5,11 @@ def func(): pass +async def coroutinefunc(): + pass + partial_func = partial(func) +partial_coroutinefunc = partial(coroutinefunc) builtin_func = print partial_builtin_func = partial(print) diff --git a/tests/roots/test-ext-autodoc/target/methods.py b/tests/roots/test-ext-autodoc/target/methods.py index 49122eb4c..ad5a6a952 100644 --- a/tests/roots/test-ext-autodoc/target/methods.py +++ b/tests/roots/test-ext-autodoc/target/methods.py @@ -19,6 +19,11 @@ class Base(): partialmeth = partialmethod(meth) + async def coroutinemeth(self): + pass + + partial_coroutinemeth = partialmethod(coroutinemeth) + class Inherited(Base): pass diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 275206526..c298e2c64 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -397,6 +397,22 @@ def test_isstaticmethod(app): assert inspect.isstaticmethod(Inherited.meth, Inherited, 'meth') is False +@pytest.mark.sphinx(testroot='ext-autodoc') +def test_iscoroutinefunction(app): + from target.functions import coroutinefunc, func, partial_coroutinefunc + from target.methods import Base + + assert inspect.iscoroutinefunction(func) is False # function + assert inspect.iscoroutinefunction(coroutinefunc) is True # coroutine + assert inspect.iscoroutinefunction(partial_coroutinefunc) is True # partial-ed coroutine + assert inspect.iscoroutinefunction(Base.meth) is False # method + assert inspect.iscoroutinefunction(Base.coroutinemeth) is True # coroutine-method + + # partial-ed coroutine-method + partial_coroutinemeth = Base.__dict__['partial_coroutinemeth'] + assert inspect.iscoroutinefunction(partial_coroutinemeth) is True + + @pytest.mark.sphinx(testroot='ext-autodoc') def test_isfunction(app): from target.functions import builtin_func, partial_builtin_func From 435ef05b99a73a8b1da1393219d3c660be1b9516 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 13 Apr 2019 23:37:16 +0900 Subject: [PATCH 4/4] Close #4777: autodoc: Support coroutine --- CHANGES | 1 + sphinx/ext/autodoc/__init__.py | 12 +++++++++++- tests/test_autodoc.py | 10 ++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 013b0733f..9cc8cab22 100644 --- a/CHANGES +++ b/CHANGES @@ -75,6 +75,7 @@ Features added functions * #6289: autodoc: :confval:`autodoc_default_options` now supports ``imported-members`` option +* #4777: autodoc: Support coroutine * #6212 autosummary: Add :confval:`autosummary_imported_members` to display imported members on autosummary * #6271: ``make clean`` is catastrophically broken if building into '.' diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 61f728ed3..2a4df2159 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1034,6 +1034,14 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ # type: (bool) -> None pass + def add_directive_header(self, sig): + # type: (str) -> None + sourcename = self.get_sourcename() + super().add_directive_header(sig) + + if inspect.iscoroutinefunction(self.object): + self.add_line(' :async:', sourcename) + class DecoratorDocumenter(FunctionDocumenter): """ @@ -1318,9 +1326,11 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: sourcename = self.get_sourcename() obj = self.parent.__dict__.get(self.object_name, self.object) + if inspect.iscoroutinefunction(obj): + self.add_line(' :async:', sourcename) if inspect.isclassmethod(obj): self.add_line(' :classmethod:', sourcename) - elif inspect.isstaticmethod(obj, cls=self.parent, name=self.object_name): + if inspect.isstaticmethod(obj, cls=self.parent, name=self.object_name): self.add_line(' :staticmethod:', sourcename) def document_members(self, all_members=False): diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 49a02cfb4..5f616b791 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -1523,6 +1523,15 @@ def test_bound_method(): @pytest.mark.usefixtures('setup_test') def test_coroutine(): + actual = do_autodoc(app, 'function', 'target.functions.coroutinefunc') + assert list(actual) == [ + '', + '.. py:function:: coroutinefunc()', + ' :module: target.functions', + ' :async:', + '', + ] + options = {"members": None} actual = do_autodoc(app, 'class', 'target.coroutine.AsyncClass', options) assert list(actual) == [ @@ -1533,6 +1542,7 @@ def test_coroutine(): ' ', ' .. py:method:: AsyncClass.do_coroutine()', ' :module: target.coroutine', + ' :async:', ' ', ' A documented coroutine function', ' '