Skip to content

Commit

Permalink
Merge pull request #8539 from godlygeek/description_typehints_for_cla…
Browse files Browse the repository at this point in the history
…sses

Make autodoc_typehints="description" work with autoclass_content="class"
  • Loading branch information
tk0miya committed Apr 3, 2021
2 parents 2b1d745 + 4c72848 commit ddb6e9c
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 4 deletions.
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

0 comments on commit ddb6e9c

Please sign in to comment.