diff --git a/CHANGES b/CHANGES index cda16787825..96390c92ec2 100644 --- a/CHANGES +++ b/CHANGES @@ -61,6 +61,8 @@ Features added * #8417: autodoc: ``:inherited-members:`` option now takes multiple classes. It allows to suppress inherited members of several classes on the module at once by specifying the option to :rst:dir:`automodule` directive +* #9792: autodoc: Add new option for ``autodoc_typehints_description_target`` to + include undocumented return values but not undocumented parameters. * #10028: Removed internal usages of JavaScript frameworks (jQuery and underscore.js) and modernised ``doctools.js`` and ``searchtools.js`` to EMCAScript 2018. diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index aa83f77d93b..255a059f9e1 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -634,8 +634,16 @@ There are also config values that you can set: When set to ``"documented"``, types will only be documented for a parameter or a return value that is already documented by the docstring. + With ``"documented_params"``, parameter types will only be annotated if the + parameter is documented in the docstring. The return type is always + annotated (except if it is ``None``). + .. versionadded:: 4.0 + .. versionadded:: 5.0 + + New option ``'documented_params'`` is added. + .. 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 a38e601bac5..97b5420e921 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -2886,7 +2886,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('autodoc_typehints', "signature", True, ENUM("signature", "description", "none", "both")) app.add_config_value('autodoc_typehints_description_target', 'all', True, - ENUM('all', 'documented')) + ENUM('all', 'documented', 'documented_params')) app.add_config_value('autodoc_type_aliases', {}, True) app.add_config_value('autodoc_typehints_format', "short", 'env', ENUM("fully-qualified", "short")) diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py index 2b5ceb4a6f3..06768168eb7 100644 --- a/sphinx/ext/autodoc/typehints.py +++ b/sphinx/ext/autodoc/typehints.py @@ -60,8 +60,14 @@ def merge_typehints(app: Sphinx, domain: str, objtype: str, contentnode: Element for field_list in field_lists: if app.config.autodoc_typehints_description_target == "all": 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]) + augment_descriptions_with_types( + field_list, annotations[fullname], force_rtype=False + ) def insert_field_list(node: Element) -> nodes.field_list: @@ -127,6 +133,7 @@ def modify_field_list(node: nodes.field_list, annotations: Dict[str, str]) -> No 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() # type: Set[str] @@ -163,10 +170,12 @@ def augment_descriptions_with_types( # 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: + 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('', annotations['return'])) + field += nodes.field_body('', nodes.paragraph('', rtype)) node += field diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index 273e08210da..ff4d714c3de 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -892,6 +892,70 @@ def test_autodoc_typehints_description_no_undoc(app): in context) +@pytest.mark.sphinx('text', testroot='ext-autodoc', + confoverrides={'autodoc_typehints': "description", + 'autodoc_typehints_description_target': 'documented_params'}) +def test_autodoc_typehints_description_no_undoc_doc_rtype(app): + # No :type: will be injected for `incr`, which does not have a description + # for its parameters or its return, just :rtype: will be injected due to + # autodoc_typehints_description_target. `tuple_args` does describe both, so + # :type: and :rtype: will be added. `nothing` has no parameters but a return + # type of None, which will be added. + (app.srcdir / 'index.rst').write_text( + '.. autofunction:: target.typehints.incr\n' + '\n' + '.. autofunction:: target.typehints.decr\n' + '\n' + ' :returns: decremented number\n' + '\n' + '.. autofunction:: target.typehints.tuple_args\n' + '\n' + ' :param x: arg\n' + ' :return: another tuple\n' + '\n' + '.. autofunction:: target.typehints.Math.nothing\n' + '\n' + '.. autofunction:: target.typehints.Math.horse\n' + '\n' + ' :return: nothing\n' + ) + app.build() + context = (app.outdir / 'index.txt').read_text() + assert ('target.typehints.incr(a, b=1)\n' + '\n' + ' Return type:\n' + ' int\n' + '\n' + 'target.typehints.decr(a, b=1)\n' + '\n' + ' Returns:\n' + ' decremented number\n' + '\n' + ' Return type:\n' + ' int\n' + '\n' + 'target.typehints.tuple_args(x)\n' + '\n' + ' Parameters:\n' + ' **x** (*Tuple**[**int**, **Union**[**int**, **str**]**]*) -- arg\n' + '\n' + ' Returns:\n' + ' another tuple\n' + '\n' + ' Return type:\n' + ' *Tuple*[int, int]\n' + '\n' + 'target.typehints.Math.nothing(self)\n' + '\n' + 'target.typehints.Math.horse(self, a, b)\n' + '\n' + ' Returns:\n' + ' nothing\n' + '\n' + ' Return type:\n' + ' None\n' == context) + + @pytest.mark.sphinx('text', testroot='ext-autodoc', confoverrides={'autodoc_typehints': "description"}) def test_autodoc_typehints_description_with_documented_init(app): @@ -944,6 +1008,31 @@ def test_autodoc_typehints_description_with_documented_init_no_undoc(app): ' **x** (*int*) -- Some integer\n' == context) +@pytest.mark.sphinx('text', testroot='ext-autodoc', + confoverrides={'autodoc_typehints': "description", + 'autodoc_typehints_description_target': 'documented_params'}) +def test_autodoc_typehints_description_with_documented_init_no_undoc_doc_rtype(app): + # see test_autodoc_typehints_description_with_documented_init_no_undoc + # returnvalue_and_documented_params should not change class or method + # docstring. + (app.srcdir / 'index.rst').write_text( + '.. autoclass:: target.typehints._ClassWithDocumentedInit\n' + ' :special-members: __init__\n' + ) + app.build() + context = (app.outdir / 'index.txt').read_text() + assert ('class target.typehints._ClassWithDocumentedInit(x)\n' + '\n' + ' Class docstring.\n' + '\n' + ' __init__(x)\n' + '\n' + ' Init docstring.\n' + '\n' + ' Parameters:\n' + ' **x** (*int*) -- Some integer\n' == context) + + @pytest.mark.sphinx('text', testroot='ext-autodoc', confoverrides={'autodoc_typehints': "description"}) def test_autodoc_typehints_description_for_invalid_node(app):