Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make autodoc_typehints="description" work with autoclass_content="class" #8539

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions doc/usage/extensions/autodoc.rst
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions sphinx/ext/autodoc/__init__.py
Expand Up @@ -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)
Expand Down
55 changes: 51 additions & 4 deletions sphinx/ext/autodoc/typehints.py
Expand Up @@ -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
Expand Down Expand Up @@ -42,8 +42,6 @@ def merge_typehints(app: Sphinx, domain: str, objtype: str, contentnode: Element
return
if app.config.autodoc_typehints != 'description':
return
if objtype == 'class' and app.config.autoclass_content not in ('init', 'both'):
return

try:
signature = cast(addnodes.desc_signature, contentnode.parent[0])
Expand All @@ -63,7 +61,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:
Expand Down Expand Up @@ -126,6 +127,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)
Expand Down
10 changes: 10 additions & 0 deletions tests/roots/test-ext-autodoc/target/typehints.py
Expand Up @@ -68,3 +68,13 @@ def missing_attr(c,
):
# type: (...) -> str
return a + (b or "")


class _ClassWithDocumentedInit:
"""Class docstring."""

def __init__(self, x: int) -> None:
"""Init docstring.

:param x: Some integer
"""
84 changes: 84 additions & 0 deletions tests/test_ext_autodoc_configs.py
Expand Up @@ -682,6 +682,90 @@ def test_autodoc_typehints_description(app):
in context)


@pytest.mark.sphinx('text', testroot='ext-autodoc',
confoverrides={'autodoc_typehints': "description",
'autodoc_typehints_description_target': 'documented'})
def test_autodoc_typehints_description_no_undoc(app):
# No :type: or :rtype: will be injected for `incr`, which does not have
# a description for its parameters or its return. `tuple_args` does
# describe them, so :type: and :rtype: will be added.
(app.srcdir / 'index.rst').write_text(
'.. autofunction:: target.typehints.incr\n'
'\n'
'.. autofunction:: target.typehints.tuple_args\n'
'\n'
' :param x: arg\n'
' :return: another tuple\n'
)
app.build()
context = (app.outdir / 'index.txt').read_text()
assert ('target.typehints.incr(a, b=1)\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'
in context)


@pytest.mark.sphinx('text', testroot='ext-autodoc',
confoverrides={'autodoc_typehints': "description"})
def test_autodoc_typehints_description_with_documented_init(app):
(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'
' Parameters:\n'
' **x** (*int*) --\n'
'\n'
' Return type:\n'
' None\n'
'\n'
' __init__(x)\n'
'\n'
' Init docstring.\n'
'\n'
' Parameters:\n'
' **x** (*int*) -- Some integer\n'
'\n'
' Return type:\n'
' None\n' == context)


@pytest.mark.sphinx('text', testroot='ext-autodoc',
confoverrides={'autodoc_typehints': "description",
'autodoc_typehints_description_target': 'documented'})
def test_autodoc_typehints_description_with_documented_init_no_undoc(app):
(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):
Expand Down