Skip to content

Commit 4d46668

Browse files
committed
Add a new extension: sphinx.ext.autodoc.typehints
1 parent 562382c commit 4d46668

File tree

3 files changed

+150
-0
lines changed

3 files changed

+150
-0
lines changed

CHANGES

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

4246
Bugs fixed
4347
----------

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.inspect import Signature
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 = Signature(obj)
42+
for param in sig.parameters.values():
43+
if param.annotation is not param.empty:
44+
annotations[param.name] = sig.format_annotation(param.annotation)
45+
if sig.return_annotation is not sig.empty:
46+
annotations['return'] = 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
@@ -442,6 +442,8 @@ class Signature:
442442
its return annotation.
443443
"""
444444

445+
empty = inspect.Signature.empty
446+
445447
def __init__(self, subject: Callable, bound_method: bool = False,
446448
has_retval: bool = True) -> None:
447449
warnings.warn('sphinx.util.inspect.Signature() is deprecated',

0 commit comments

Comments
 (0)