Add a new extension: sphinx.ext.autodoc.typehints

This commit is contained in:
Takeshi KOMIYA 2020-01-12 01:54:57 +09:00
parent 2e22e96061
commit 5397664d42
6 changed files with 191 additions and 0 deletions

View File

@ -39,6 +39,10 @@ Features added
* #2755: autodoc: Support type_comment style (ex. ``# type: (str) -> str``) * #2755: autodoc: Support type_comment style (ex. ``# type: (str) -> str``)
annotation (python3.8+ or `typed_ast <https://github.com/python/typed_ast>`_ annotation (python3.8+ or `typed_ast <https://github.com/python/typed_ast>`_
is required) is required)
* #6418: autodoc: Add a new extension ``sphinx.ext.autodoc.typehints``. It shows
typehints as object description if ``autodoc_typehints = "description"`` set.
This is an experimental extension and it will be integrated into autodoc core
in Sphinx-3.0
* SphinxTranslator now calls visitor/departure method for super node class if * SphinxTranslator now calls visitor/departure method for super node class if
visitor/departure method for original node class not found visitor/departure method for original node class not found
* #6418: Add new event: :event:`object-description-transform` * #6418: Add new event: :event:`object-description-transform`

View File

@ -567,3 +567,24 @@ member should be included in the documentation by using the following event:
``inherited_members``, ``undoc_members``, ``show_inheritance`` and ``inherited_members``, ``undoc_members``, ``show_inheritance`` and
``noindex`` that are true if the flag option of same name was given to the ``noindex`` that are true if the flag option of same name was given to the
auto directive auto directive
Generating documents from type annotations
------------------------------------------
As an experimental feature, autodoc provides ``sphinx.ext.autodoc.typehints`` as
an additional extension. It extends autodoc itself to generate function document
from its type annotations.
To enable the feature, please add ``sphinx.ext.autodoc.typehints`` to list of
extensions and set `'description'` to :confval:`autodoc_typehints`:
.. code-block:: python
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.autodoc.typehints']
autodoc_typehints = 'description'
.. versionadded:: 2.4
Added as an experimental feature. This will be integrated into autodoc core
in Sphinx-3.0.

View File

@ -0,0 +1,144 @@
"""
sphinx.ext.autodoc.typehints
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Generating content for autodoc using typehints
:copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import re
from typing import Any, Dict, Iterable
from typing import cast
from docutils import nodes
from docutils.nodes import Element
from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.config import ENUM
from sphinx.util import inspect, typing
def config_inited(app, config):
if config.autodoc_typehints == 'description':
# HACK: override this to make autodoc suppressing typehints in signatures
config.autodoc_typehints = 'none'
# preserve user settings
app._autodoc_typehints_description = True
else:
app._autodoc_typehints_description = False
def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any,
options: Dict, args: str, retann: str) -> None:
"""Record type hints to env object."""
try:
if callable(obj):
annotations = app.env.temp_data.setdefault('annotations', {}).setdefault(name, {})
sig = inspect.signature(obj)
for param in sig.parameters.values():
if param.annotation is not param.empty:
annotations[param.name] = typing.stringify(param.annotation)
if sig.return_annotation is not sig.empty:
annotations['return'] = typing.stringify(sig.return_annotation)
except TypeError:
pass
def merge_typehints(app: Sphinx, domain: str, objtype: str, contentnode: Element) -> None:
if domain != 'py':
return
if app._autodoc_typehints_description is False: # type: ignore
return
signature = cast(addnodes.desc_signature, contentnode.parent[0])
fullname = '.'.join([signature['module'], signature['fullname']])
annotations = app.env.temp_data.get('annotations', {})
if annotations.get(fullname, {}):
field_lists = [n for n in contentnode if isinstance(n, nodes.field_list)]
if field_lists == []:
field_list = insert_field_list(contentnode)
field_lists.append(field_list)
for field_list in field_lists:
modify_field_list(field_list, annotations[fullname])
def insert_field_list(node: Element) -> nodes.field_list:
field_list = nodes.field_list()
desc = [n for n in node if isinstance(n, addnodes.desc)]
if desc:
# insert just before sub object descriptions (ex. methods, nested classes, etc.)
index = node.index(desc[0])
node.insert(index - 1, [field_list])
else:
node += field_list
return field_list
def modify_field_list(node: nodes.field_list, annotations: Dict[str, str]) -> None:
arguments = {} # type: Dict[str, Dict[str, bool]]
fields = cast(Iterable[nodes.field], node)
for field in fields:
field_name = field[0].astext()
parts = re.split(' +', field_name)
if parts[0] == 'param':
if len(parts) == 2:
# :param xxx:
arg = arguments.setdefault(parts[1], {})
arg['param'] = True
elif len(parts) > 2:
# :param xxx yyy:
name = ' '.join(parts[2:])
arg = arguments.setdefault(name, {})
arg['param'] = True
arg['type'] = True
elif parts[0] == 'type':
name = ' '.join(parts[1:])
arg = arguments.setdefault(name, {})
arg['type'] = True
elif parts[0] == 'rtype':
arguments['return'] = {'type': True}
for name, annotation in annotations.items():
if name == 'return':
continue
arg = arguments.get(name, {})
field = nodes.field()
if arg.get('param') and arg.get('type'):
# both param and type are already filled manually
continue
elif arg.get('param'):
# only param: fill type field
field += nodes.field_name('', 'type ' + name)
field += nodes.field_body('', nodes.paragraph('', annotation))
elif arg.get('type'):
# only type: It's odd...
field += nodes.field_name('', 'param ' + name)
field += nodes.field_body('', nodes.paragraph('', ''))
else:
# both param and type are not found
field += nodes.field_name('', 'param ' + annotation + ' ' + name)
field += nodes.field_body('', nodes.paragraph('', ''))
node += field
if 'return' in annotations and 'return' not in arguments:
field = nodes.field()
field += nodes.field_name('', 'rtype')
field += nodes.field_body('', nodes.paragraph('', annotation))
node += field
def setup(app):
app.setup_extension('sphinx.ext.autodoc')
app.config.values['autodoc_typehints'] = ('signature', True,
ENUM("signature", "description", "none"))
app.connect('config-inited', config_inited)
app.connect('autodoc-process-signature', record_typehints)
app.connect('object-description-transform', merge_typehints)

View File

@ -450,6 +450,8 @@ class Signature:
its return annotation. its return annotation.
""" """
empty = inspect.Signature.empty
def __init__(self, subject: Callable, bound_method: bool = False, def __init__(self, subject: Callable, bound_method: bool = False,
has_retval: bool = True) -> None: has_retval: bool = True) -> None:
warnings.warn('sphinx.util.inspect.Signature() is deprecated', warnings.warn('sphinx.util.inspect.Signature() is deprecated',

View File

@ -7,3 +7,5 @@
.. automodule:: autodoc_dummy_bar .. automodule:: autodoc_dummy_bar
:members: :members:
.. autofunction:: target.typehints.incr

View File

@ -540,6 +540,24 @@ def test_autodoc_typehints_none(app):
] ]
@pytest.mark.sphinx('text', testroot='ext-autodoc',
confoverrides={'extensions': ['sphinx.ext.autodoc.typehints'],
'autodoc_typehints': 'description'})
def test_autodoc_typehints_description(app):
app.build()
context = (app.outdir / 'index.txt').text()
assert ('target.typehints.incr(a, b=1)\n'
'\n'
' Parameters:\n'
' * **a** (*int*) --\n'
'\n'
' * **b** (*int*) --\n'
'\n'
' Return type:\n'
' int\n'
in context)
@pytest.mark.sphinx('html', testroot='ext-autodoc') @pytest.mark.sphinx('html', testroot='ext-autodoc')
@pytest.mark.filterwarnings('ignore:autodoc_default_flags is now deprecated.') @pytest.mark.filterwarnings('ignore:autodoc_default_flags is now deprecated.')
def test_merge_autodoc_default_flags1(app): def test_merge_autodoc_default_flags1(app):