From 4785f32ddf2508e85a3495c6447ff6415e8a2a29 Mon Sep 17 00:00:00 2001 From: Matt Wozniski Date: Mon, 14 Dec 2020 17:38:37 -0500 Subject: [PATCH] Add autodoc_typehint_undoc option Previously, if autodoc_typehints="description", a :type: field would be added for every parameter and return type appearing in the annotation, including **kwargs and underscore-prefixed parameters that are meant to be private, as well as None return types. This commit introduces a new option, "autodoc_typehint_undoc". By default this option is True, requesting the old behavior. By setting this option to False, :type: and :rtype: fields will only be added for annotated parameters or return types if there is already a corresponding :param: or :return: field, to put users in control over whether a given parameter is documented or not. --- doc/usage/extensions/autodoc.rst | 13 ++++++++ sphinx/ext/autodoc/__init__.py | 2 ++ sphinx/ext/autodoc/typehints.py | 53 ++++++++++++++++++++++++++++++-- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index 034c86e11..5ee71c782 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -571,6 +571,19 @@ There are also config values that you can set: New option ``'description'`` is added. +.. confval:: autodoc_typehints_description_target + + This value controls whether the types of undocumented parameters and return + values are documented when ``autodoc_typehints`` is set to ``description``. + + The default value is ``"all"``, meaning that types are documented for all + parameters and return values, whether they are documented or not. + + When set to ``"documented"``, types will only be documented for a parameter + or a return value that is already documented by the docstring. + + .. versionadded:: 4.0 + .. confval:: autodoc_type_aliases A dictionary for users defined `type aliases`__ that maps a type name to the diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 3d33b6a8e..57499ec6f 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -2656,6 +2656,8 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('autodoc_mock_imports', [], True) app.add_config_value('autodoc_typehints', "signature", True, ENUM("signature", "description", "none")) + app.add_config_value('autodoc_typehints_description_target', 'all', True, + ENUM('all', 'documented')) app.add_config_value('autodoc_type_aliases', {}, True) app.add_config_value('autodoc_warningiserror', True, True) app.add_config_value('autodoc_inherit_docstrings', True, True) diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py index 9811bdb55..f2154b9ab 100644 --- a/sphinx/ext/autodoc/typehints.py +++ b/sphinx/ext/autodoc/typehints.py @@ -10,7 +10,7 @@ import re from collections import OrderedDict -from typing import Any, Dict, Iterable, cast +from typing import Any, Dict, Iterable, Set, cast from docutils import nodes from docutils.nodes import Element @@ -63,7 +63,10 @@ def merge_typehints(app: Sphinx, domain: str, objtype: str, contentnode: Element field_lists.append(field_list) for field_list in field_lists: - modify_field_list(field_list, annotations[fullname]) + if app.config.autodoc_typehints_description_target == "all": + modify_field_list(field_list, annotations[fullname]) + else: + augment_descriptions_with_types(field_list, annotations[fullname]) def insert_field_list(node: Element) -> nodes.field_list: @@ -126,6 +129,52 @@ def modify_field_list(node: nodes.field_list, annotations: Dict[str, str]) -> No node += field +def augment_descriptions_with_types( + node: nodes.field_list, + annotations: Dict[str, str], +) -> None: + fields = cast(Iterable[nodes.field], node) + has_description = set() # type: Set[str] + has_type = set() # type: Set[str] + 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] == 'return': + 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 in annotations: + if name == 'return': + continue + 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('', annotations[name])) + node += field + + # Add 'rtype' if 'return' is present and 'rtype' isn't. + if 'return' in annotations: + if 'return' in has_description and 'return' not in has_type: + field = nodes.field() + field += nodes.field_name('', 'rtype') + field += nodes.field_body('', nodes.paragraph('', annotations['return'])) + node += field + + def setup(app: Sphinx) -> Dict[str, Any]: app.connect('autodoc-process-signature', record_typehints) app.connect('object-description-transform', merge_typehints)