summaryrefslogtreecommitdiffstats
path: root/sphinx/ext/autodoc/typehints.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/ext/autodoc/typehints.py')
-rw-r--r--sphinx/ext/autodoc/typehints.py219
1 files changed, 219 insertions, 0 deletions
diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py
new file mode 100644
index 0000000..79906fb
--- /dev/null
+++ b/sphinx/ext/autodoc/typehints.py
@@ -0,0 +1,219 @@
+"""Generating content for autodoc using typehints"""
+
+from __future__ import annotations
+
+import re
+from collections.abc import Iterable
+from typing import TYPE_CHECKING, Any, cast
+
+from docutils import nodes
+
+import sphinx
+from sphinx import addnodes
+from sphinx.util import inspect
+from sphinx.util.typing import stringify_annotation
+
+if TYPE_CHECKING:
+ from docutils.nodes import Element
+
+ from sphinx.application import Sphinx
+
+
+def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any,
+ options: dict, args: str, retann: str) -> None:
+ """Record type hints to env object."""
+ if app.config.autodoc_typehints_format == 'short':
+ mode = 'smart'
+ else:
+ mode = 'fully-qualified'
+
+ try:
+ if callable(obj):
+ annotations = app.env.temp_data.setdefault('annotations', {})
+ annotation = annotations.setdefault(name, {})
+ sig = inspect.signature(obj, type_aliases=app.config.autodoc_type_aliases)
+ for param in sig.parameters.values():
+ if param.annotation is not param.empty:
+ annotation[param.name] = stringify_annotation(param.annotation, mode)
+ if sig.return_annotation is not sig.empty:
+ annotation['return'] = stringify_annotation(sig.return_annotation, mode)
+ except (TypeError, ValueError):
+ pass
+
+
+def merge_typehints(app: Sphinx, domain: str, objtype: str, contentnode: Element) -> None:
+ if domain != 'py':
+ return
+ if app.config.autodoc_typehints not in ('both', 'description'):
+ return
+
+ try:
+ signature = cast(addnodes.desc_signature, contentnode.parent[0])
+ if signature['module']:
+ fullname = '.'.join([signature['module'], signature['fullname']])
+ else:
+ fullname = signature['fullname']
+ except KeyError:
+ # signature node does not have valid context info for the target object
+ return
+
+ 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:
+ if app.config.autodoc_typehints_description_target == "all":
+ if objtype == 'class':
+ modify_field_list(field_list, annotations[fullname], suppress_rtype=True)
+ else:
+ modify_field_list(field_list, annotations[fullname])
+ elif app.config.autodoc_typehints_description_target == "documented_params":
+ augment_descriptions_with_types(
+ field_list, annotations[fullname], force_rtype=True,
+ )
+ else:
+ augment_descriptions_with_types(
+ field_list, annotations[fullname], force_rtype=False,
+ )
+
+
+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],
+ suppress_rtype: bool = False) -> None:
+ arguments: 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
+
+ if '*' + name in arguments:
+ name = '*' + name
+ arguments.get(name)
+ elif '**' + name in arguments:
+ name = '**' + name
+ arguments.get(name)
+ else:
+ arg = arguments.get(name, {})
+
+ if not arg.get('type'):
+ field = nodes.field()
+ field += nodes.field_name('', 'type ' + name)
+ field += nodes.field_body('', nodes.paragraph('', annotation))
+ node += field
+ if not arg.get('param'):
+ field = nodes.field()
+ field += nodes.field_name('', 'param ' + name)
+ field += nodes.field_body('', nodes.paragraph('', ''))
+ node += field
+
+ if 'return' in annotations and 'return' not in arguments:
+ annotation = annotations['return']
+ if annotation == 'None' and suppress_rtype:
+ return
+
+ field = nodes.field()
+ field += nodes.field_name('', 'rtype')
+ field += nodes.field_body('', nodes.paragraph('', annotation))
+ node += field
+
+
+def augment_descriptions_with_types(
+ node: nodes.field_list,
+ annotations: dict[str, str],
+ force_rtype: bool,
+) -> None:
+ fields = cast(Iterable[nodes.field], node)
+ has_description: set[str] = set()
+ has_type: set[str] = set()
+ for field in fields:
+ field_name = field[0].astext()
+ parts = re.split(' +', field_name)
+ if parts[0] == 'param':
+ if len(parts) == 2:
+ # :param xxx:
+ has_description.add(parts[1])
+ elif len(parts) > 2:
+ # :param xxx yyy:
+ name = ' '.join(parts[2:])
+ has_description.add(name)
+ has_type.add(name)
+ elif parts[0] == 'type':
+ name = ' '.join(parts[1:])
+ has_type.add(name)
+ elif parts[0] in ('return', 'returns'):
+ has_description.add('return')
+ elif parts[0] == 'rtype':
+ has_type.add('return')
+
+ # Add 'type' for parameters with a description but no declared type.
+ for name, annotation in annotations.items():
+ if name in ('return', 'returns'):
+ continue
+
+ if '*' + name in has_description:
+ name = '*' + name
+ elif '**' + name in has_description:
+ name = '**' + name
+
+ if name in has_description and name not in has_type:
+ field = nodes.field()
+ field += nodes.field_name('', 'type ' + name)
+ field += nodes.field_body('', nodes.paragraph('', annotation))
+ node += field
+
+ # Add 'rtype' if 'return' is present and 'rtype' isn't.
+ if 'return' in annotations:
+ rtype = annotations['return']
+ if 'return' not in has_type and ('return' in has_description or
+ (force_rtype and rtype != "None")):
+ field = nodes.field()
+ field += nodes.field_name('', 'rtype')
+ field += nodes.field_body('', nodes.paragraph('', rtype))
+ node += field
+
+
+def setup(app: Sphinx) -> dict[str, Any]:
+ app.connect('autodoc-process-signature', record_typehints)
+ app.connect('object-description-transform', merge_typehints)
+
+ return {
+ 'version': sphinx.__display_version__,
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }