summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTakeshi KOMIYA <i.tkomiya@gmail.com>2019-04-24 00:23:55 +0900
committerGitHub <noreply@github.com>2019-04-24 00:23:55 +0900
commitb49ef12e6aed3481188da05955ee1782ebf70778 (patch)
tree3ea5daac16fb678120b6bab671a2ac4d83fe903c
parent80e50c1938ba3b17f85a8958b8baa7319c9fe66f (diff)
parent435ef05b99a73a8b1da1393219d3c660be1b9516 (diff)
downloadsphinx-git-b49ef12e6aed3481188da05955ee1782ebf70778.tar.gz
Merge pull request #6295 from tk0miya/refactor_py_domain4
autodoc: Support coroutine (refs: #4777)
-rw-r--r--CHANGES6
-rw-r--r--doc/usage/restructuredtext/domains.rst12
-rw-r--r--sphinx/domains/python.py25
-rw-r--r--sphinx/ext/autodoc/__init__.py12
-rw-r--r--sphinx/pycode/parser.py4
-rw-r--r--sphinx/util/inspect.py16
-rw-r--r--tests/roots/test-ext-autodoc/target/functions.py4
-rw-r--r--tests/roots/test-ext-autodoc/target/methods.py5
-rw-r--r--tests/test_autodoc.py10
-rw-r--r--tests/test_domain_py.py33
-rw-r--r--tests/test_pycode_parser.py15
-rw-r--r--tests/test_util_inspect.py16
12 files changed, 144 insertions, 14 deletions
diff --git a/CHANGES b/CHANGES
index e07b67059..9cc8cab22 100644
--- a/CHANGES
+++ b/CHANGES
@@ -75,11 +75,13 @@ 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 '.'
-* 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/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/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/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_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',
' '
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"
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'
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
@@ -398,6 +398,22 @@ def test_isstaticmethod(app):
@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 target.functions import func, partial_func