Skip to content

Commit 67817ac

Browse files
committed
Add a new extension: sphinx.ext.autodoc.typehints
1 parent 2e22e96 commit 67817ac

File tree

6 files changed

+191
-0
lines changed

6 files changed

+191
-0
lines changed

CHANGES

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ Features added
4242
* SphinxTranslator now calls visitor/departure method for super node class if
4343
visitor/departure method for original node class not found
4444
* #6418: Add new event: :event:`object-description-transform`
45+
* #6418: autodoc: Add a new extension ``sphinx.ext.autodoc.typehints``. It shows
46+
typehints as object description if ``autodoc_typehints = "description"`` set.
47+
This is an experimental extension and it will be integrated into autodoc in
48+
Sphinx-3.0.
4549

4650
Bugs fixed
4751
----------

doc/usage/extensions/autodoc.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,3 +567,24 @@ member should be included in the documentation by using the following event:
567567
``inherited_members``, ``undoc_members``, ``show_inheritance`` and
568568
``noindex`` that are true if the flag option of same name was given to the
569569
auto directive
570+
571+
Generating documents from type annotations
572+
------------------------------------------
573+
574+
As an experimental feature, autodoc provides ``sphinx.ext.autodoc.typehints`` as
575+
an additional extension. It extends autodoc itself to generate function document
576+
from its type annotations.
577+
578+
To enable the feature, please add ``sphinx.ext.autodoc.typehints`` to list of
579+
extensions and set `'description'` to :confval:`autodoc_typehints`:
580+
581+
.. code-block:: python
582+
583+
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.autodoc.typehints']
584+
585+
autodoc_typehints = 'description'
586+
587+
.. versionadded:: 2.4
588+
589+
Added as an experimental feature. This will be integrated into autodoc core
590+
in Sphinx-3.0.

sphinx/ext/autodoc/typehints.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"""
2+
sphinx.ext.autodoc.typehints
3+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4+
5+
Generating content for autodoc using typehints
6+
7+
:copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
8+
:license: BSD, see LICENSE for details.
9+
"""
10+
11+
import re
12+
from typing import Any, Dict, Iterable
13+
from typing import cast
14+
15+
from docutils import nodes
16+
from docutils.nodes import Element
17+
18+
from sphinx import addnodes
19+
from sphinx.application import Sphinx
20+
from sphinx.config import ENUM
21+
from sphinx.util import inspect, typing
22+
23+
24+
def config_inited(app, config):
25+
if config.autodoc_typehints == 'description':
26+
# HACK: override this to make autodoc suppressing typehints in signatures
27+
config.autodoc_typehints = 'none'
28+
29+
# preserve user settings
30+
app._autodoc_typehints_description = True
31+
else:
32+
app._autodoc_typehints_description = False
33+
34+
35+
def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any,
36+
options: Dict, args: str, retann: str) -> None:
37+
"""Record type hints to env object."""
38+
try:
39+
if callable(obj):
40+
annotations = app.env.temp_data.setdefault('annotations', {}).setdefault(name, {})
41+
sig = inspect.signature(obj)
42+
for param in sig.parameters.values():
43+
if param.annotation is not param.empty:
44+
annotations[param.name] = typing.stringify(param.annotation)
45+
if sig.return_annotation is not sig.empty:
46+
annotations['return'] = typing.stringify(sig.return_annotation)
47+
except TypeError:
48+
pass
49+
50+
51+
def merge_typehints(app: Sphinx, domain: str, objtype: str, contentnode: Element) -> None:
52+
if domain != 'py':
53+
return
54+
if app._autodoc_typehints_description is False: # type: ignore
55+
return
56+
57+
signature = cast(addnodes.desc_signature, contentnode.parent[0])
58+
fullname = '.'.join([signature['module'], signature['fullname']])
59+
annotations = app.env.temp_data.get('annotations', {})
60+
if annotations.get(fullname, {}):
61+
field_lists = [n for n in contentnode if isinstance(n, nodes.field_list)]
62+
if field_lists == []:
63+
field_list = insert_field_list(contentnode)
64+
field_lists.append(field_list)
65+
66+
for field_list in field_lists:
67+
modify_field_list(field_list, annotations[fullname])
68+
69+
70+
def insert_field_list(node: Element) -> nodes.field_list:
71+
field_list = nodes.field_list()
72+
desc = [n for n in node if isinstance(n, addnodes.desc)]
73+
if desc:
74+
# insert just before sub object descriptions (ex. methods, nested classes, etc.)
75+
index = node.index(desc[0])
76+
node.insert(index - 1, [field_list])
77+
else:
78+
node += field_list
79+
80+
return field_list
81+
82+
83+
def modify_field_list(node: nodes.field_list, annotations: Dict[str, str]) -> None:
84+
arguments = {} # type: Dict[str, Dict[str, bool]]
85+
fields = cast(Iterable[nodes.field], node)
86+
for field in fields:
87+
field_name = field[0].astext()
88+
parts = re.split(' +', field_name)
89+
if parts[0] == 'param':
90+
if len(parts) == 2:
91+
# :param xxx:
92+
arg = arguments.setdefault(parts[1], {})
93+
arg['param'] = True
94+
elif len(parts) > 2:
95+
# :param xxx yyy:
96+
name = ' '.join(parts[2:])
97+
arg = arguments.setdefault(name, {})
98+
arg['param'] = True
99+
arg['type'] = True
100+
elif parts[0] == 'type':
101+
name = ' '.join(parts[1:])
102+
arg = arguments.setdefault(name, {})
103+
arg['type'] = True
104+
elif parts[0] == 'rtype':
105+
arguments['return'] = {'type': True}
106+
107+
for name, annotation in annotations.items():
108+
if name == 'return':
109+
continue
110+
111+
arg = arguments.get(name, {})
112+
field = nodes.field()
113+
if arg.get('param') and arg.get('type'):
114+
# both param and type are already filled manually
115+
continue
116+
elif arg.get('param'):
117+
# only param: fill type field
118+
field += nodes.field_name('', 'type ' + name)
119+
field += nodes.field_body('', nodes.paragraph('', annotation))
120+
elif arg.get('type'):
121+
# only type: It's odd...
122+
field += nodes.field_name('', 'param ' + name)
123+
field += nodes.field_body('', nodes.paragraph('', ''))
124+
else:
125+
# both param and type are not found
126+
field += nodes.field_name('', 'param ' + annotation + ' ' + name)
127+
field += nodes.field_body('', nodes.paragraph('', ''))
128+
129+
node += field
130+
131+
if 'return' in annotations and 'return' not in arguments:
132+
field = nodes.field()
133+
field += nodes.field_name('', 'rtype')
134+
field += nodes.field_body('', nodes.paragraph('', annotation))
135+
node += field
136+
137+
138+
def setup(app):
139+
app.setup_extension('sphinx.ext.autodoc')
140+
app.config.values['autodoc_typehints'] = ('signature', True,
141+
ENUM("signature", "description", "none"))
142+
app.connect('config-inited', config_inited)
143+
app.connect('autodoc-process-signature', record_typehints)
144+
app.connect('object-description-transform', merge_typehints)

sphinx/util/inspect.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,8 @@ class Signature:
450450
its return annotation.
451451
"""
452452

453+
empty = inspect.Signature.empty
454+
453455
def __init__(self, subject: Callable, bound_method: bool = False,
454456
has_retval: bool = True) -> None:
455457
warnings.warn('sphinx.util.inspect.Signature() is deprecated',

tests/roots/test-ext-autodoc/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@
77

88
.. automodule:: autodoc_dummy_bar
99
:members:
10+
11+
.. autofunction:: target.typehints.incr

tests/test_ext_autodoc_configs.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,24 @@ def test_autodoc_typehints_none(app):
540540
]
541541

542542

543+
@pytest.mark.sphinx('text', testroot='ext-autodoc',
544+
confoverrides={'extensions': ['sphinx.ext.autodoc.typehints'],
545+
'autodoc_typehints': 'description'})
546+
def test_autodoc_typehints_description(app):
547+
app.build()
548+
context = (app.outdir / 'index.txt').text()
549+
assert ('target.typehints.incr(a, b=1)\n'
550+
'\n'
551+
' Parameters:\n'
552+
' * **a** (*int*) --\n'
553+
'\n'
554+
' * **b** (*int*) --\n'
555+
'\n'
556+
' Return type:\n'
557+
' int\n'
558+
in context)
559+
560+
543561
@pytest.mark.sphinx('html', testroot='ext-autodoc')
544562
@pytest.mark.filterwarnings('ignore:autodoc_default_flags is now deprecated.')
545563
def test_merge_autodoc_default_flags1(app):

0 commit comments

Comments
 (0)