Merge pull request #6295 from tk0miya/refactor_py_domain4

autodoc: Support coroutine (refs: #4777)
This commit is contained in:
Takeshi KOMIYA 2019-04-24 00:23:55 +09:00 committed by GitHub
commit b49ef12e6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 144 additions and 14 deletions

View File

@ -75,11 +75,13 @@ Features added
functions functions
* #6289: autodoc: :confval:`autodoc_default_options` now supports * #6289: autodoc: :confval:`autodoc_default_options` now supports
``imported-members`` option ``imported-members`` option
* #4777: autodoc: Support coroutine
* #6212 autosummary: Add :confval:`autosummary_imported_members` to display * #6212 autosummary: Add :confval:`autosummary_imported_members` to display
imported members on autosummary imported members on autosummary
* #6271: ``make clean`` is catastrophically broken if building into '.' * #6271: ``make clean`` is catastrophically broken if building into '.'
* Add ``:classmethod:`` and ``:staticmethod:`` options to :rst:dir:`py:method` * #4777: py domain: Add ``:async:`` option to :rst:dir:`py:function` directive
directive * py domain: Add ``:async:``, ``:classmethod:`` and ``:staticmethod:`` options
to :rst:dir:`py:method` directive
Bugs fixed Bugs fixed
---------- ----------

View File

@ -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 This information can (in any ``py`` directive) optionally be given in a
structured form, see :ref:`info-field-lists`. 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 .. rst:directive:: .. py:data:: name
Describes global data in a module, including both variables and values used 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 described for ``function``. See also :ref:`signatures` and
:ref:`info-field-lists`. :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 The ``classmethod`` option and ``staticmethod`` option can be given (with
no value) to indicate the method is a class method (or a static method). no value) to indicate the method is a class method (or a static method).
.. versionchanged:: 2.1 .. versionchanged:: 2.1
``:classmethod:`` and ``:staticmethod:`` options added. ``:async:``, ``:classmethod:`` and ``:staticmethod:`` options added.
.. rst:directive:: .. py:staticmethod:: name(parameters) .. rst:directive:: .. py:staticmethod:: name(parameters)

View File

@ -438,6 +438,18 @@ class PyModulelevel(PyObject):
class PyFunction(PyObject): class PyFunction(PyObject):
"""Description of a function.""" """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): def needs_arglist(self):
# type: () -> bool # type: () -> bool
return True return True
@ -573,6 +585,7 @@ class PyMethod(PyObject):
option_spec = PyObject.option_spec.copy() option_spec = PyObject.option_spec.copy()
option_spec.update({ option_spec.update({
'async': directives.flag,
'classmethod': directives.flag, 'classmethod': directives.flag,
'staticmethod': directives.flag, 'staticmethod': directives.flag,
}) })
@ -583,10 +596,16 @@ class PyMethod(PyObject):
def get_signature_prefix(self, sig): def get_signature_prefix(self, sig):
# type: (str) -> str # type: (str) -> str
prefix = []
if 'async' in self.options:
prefix.append('async')
if 'staticmethod' in self.options: if 'staticmethod' in self.options:
return 'static ' prefix.append('static')
elif 'classmethod' in self.options: if 'classmethod' in self.options:
return 'classmethod ' prefix.append('classmethod')
if prefix:
return ' '.join(prefix) + ' '
else: else:
return '' return ''

View File

@ -1034,6 +1034,14 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
# type: (bool) -> None # type: (bool) -> None
pass 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): class DecoratorDocumenter(FunctionDocumenter):
""" """
@ -1318,9 +1326,11 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
sourcename = self.get_sourcename() sourcename = self.get_sourcename()
obj = self.parent.__dict__.get(self.object_name, self.object) obj = self.parent.__dict__.get(self.object_name, self.object)
if inspect.iscoroutinefunction(obj):
self.add_line(' :async:', sourcename)
if inspect.isclassmethod(obj): if inspect.isclassmethod(obj):
self.add_line(' :classmethod:', sourcename) 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) self.add_line(' :staticmethod:', sourcename)
def document_members(self, all_members=False): def document_members(self, all_members=False):

View File

@ -381,6 +381,10 @@ class VariableCommentPicker(ast.NodeVisitor):
self.context.pop() self.context.pop()
self.current_function = None self.current_function = None
def visit_AsyncFunctionDef(self, node):
# type: (ast.AsyncFunctionDef) -> None
self.visit_FunctionDef(node) # type: ignore
class DefinitionFinder(TokenProcessor): class DefinitionFinder(TokenProcessor):
def __init__(self, lines): def __init__(self, lines):

View File

@ -15,7 +15,7 @@ import re
import sys import sys
import typing import typing
import warnings import warnings
from functools import partial from functools import partial, partialmethod
from inspect import ( # NOQA from inspect import ( # NOQA
isclass, ismethod, ismethoddescriptor, isroutine isclass, ismethod, ismethoddescriptor, isroutine
) )
@ -129,7 +129,7 @@ def isenumattribute(x):
def ispartial(obj): def ispartial(obj):
# type: (Any) -> bool # type: (Any) -> bool
"""Check if the object is partial.""" """Check if the object is partial."""
return isinstance(obj, partial) return isinstance(obj, (partial, partialmethod))
def isclassmethod(obj): def isclassmethod(obj):
@ -212,6 +212,18 @@ def isbuiltin(obj):
return inspect.isbuiltin(obj) or ispartial(obj) and inspect.isbuiltin(obj.func) 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): def safe_getattr(obj, name, *defargs):
# type: (Any, str, str) -> object # type: (Any, str, str) -> object
"""A getattr() that turns all exceptions into AttributeErrors.""" """A getattr() that turns all exceptions into AttributeErrors."""

View File

@ -5,7 +5,11 @@ def func():
pass pass
async def coroutinefunc():
pass
partial_func = partial(func) partial_func = partial(func)
partial_coroutinefunc = partial(coroutinefunc)
builtin_func = print builtin_func = print
partial_builtin_func = partial(print) partial_builtin_func = partial(print)

View File

@ -19,6 +19,11 @@ class Base():
partialmeth = partialmethod(meth) partialmeth = partialmethod(meth)
async def coroutinemeth(self):
pass
partial_coroutinemeth = partialmethod(coroutinemeth)
class Inherited(Base): class Inherited(Base):
pass pass

View File

@ -1523,6 +1523,15 @@ def test_bound_method():
@pytest.mark.usefixtures('setup_test') @pytest.mark.usefixtures('setup_test')
def test_coroutine(): def test_coroutine():
actual = do_autodoc(app, 'function', 'target.functions.coroutinefunc')
assert list(actual) == [
'',
'.. py:function:: coroutinefunc()',
' :module: target.functions',
' :async:',
'',
]
options = {"members": None} options = {"members": None}
actual = do_autodoc(app, 'class', 'target.coroutine.AsyncClass', options) actual = do_autodoc(app, 'class', 'target.coroutine.AsyncClass', options)
assert list(actual) == [ assert list(actual) == [
@ -1533,6 +1542,7 @@ def test_coroutine():
' ', ' ',
' .. py:method:: AsyncClass.do_coroutine()', ' .. py:method:: AsyncClass.do_coroutine()',
' :module: target.coroutine', ' :module: target.coroutine',
' :async:',
' ', ' ',
' A documented coroutine function', ' A documented coroutine function',
' ' ' '

View File

@ -304,15 +304,24 @@ def test_pydata(app):
def test_pyfunction(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') domain = app.env.get_domain('py')
doctree = restructuredtext.parse(app, text) doctree = restructuredtext.parse(app, text)
assert_node(doctree, (addnodes.index, 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_parameterlist, ()])],
[desc_content, ()])])) [desc_content, ()])]))
assert 'func' in domain.objects assert 'func1' in domain.objects
assert domain.objects['func'] == ('index', 'function') assert domain.objects['func1'] == ('index', 'function')
assert 'func2' in domain.objects
assert domain.objects['func2'] == ('index', 'function')
def test_pymethod_options(app): def test_pymethod_options(app):
@ -322,7 +331,9 @@ def test_pymethod_options(app):
" .. py:method:: meth2\n" " .. py:method:: meth2\n"
" :classmethod:\n" " :classmethod:\n"
" .. py:method:: meth3\n" " .. py:method:: meth3\n"
" :staticmethod:\n") " :staticmethod:\n"
" .. py:method:: meth4\n"
" :async:\n")
domain = app.env.get_domain('py') domain = app.env.get_domain('py')
doctree = restructuredtext.parse(app, text) doctree = restructuredtext.parse(app, text)
assert_node(doctree, (addnodes.index, assert_node(doctree, (addnodes.index,
@ -333,6 +344,8 @@ def test_pymethod_options(app):
addnodes.index, addnodes.index,
desc, desc,
addnodes.index, addnodes.index,
desc,
addnodes.index,
desc)])])) desc)])]))
# method # method
@ -364,6 +377,16 @@ def test_pymethod_options(app):
assert 'Class.meth3' in domain.objects assert 'Class.meth3' in domain.objects
assert domain.objects['Class.meth3'] == ('index', 'method') 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): def test_pyclassmethod(app):
text = (".. py:class:: Class\n" text = (".. py:class:: Class\n"

View File

@ -314,6 +314,21 @@ def test_decorators():
'Foo.method': ('def', 13, 15)} '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(): def test_formfeed_char():
source = ('class Foo:\n' source = ('class Foo:\n'
'\f\n' '\f\n'

View File

@ -397,6 +397,22 @@ def test_isstaticmethod(app):
assert inspect.isstaticmethod(Inherited.meth, Inherited, 'meth') is False 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') @pytest.mark.sphinx(testroot='ext-autodoc')
def test_isfunction(app): def test_isfunction(app):
from target.functions import builtin_func, partial_builtin_func from target.functions import builtin_func, partial_builtin_func