diff --git a/CHANGES b/CHANGES index d42b87440..0f65675eb 100644 --- a/CHANGES +++ b/CHANGES @@ -39,6 +39,8 @@ Bugs fixed * #9575: autodoc: The annotation of return value should not be shown when ``autodoc_typehints="description"`` +* #9648: autodoc: ``*args`` and ``**kwargs`` entries are duplicated when + ``autodoc_typehints="description"`` * #10456: py domain: ``:meta:`` fields are displayed if docstring contains two or more meta-field diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py index d912cd7d5..98c51e9e9 100644 --- a/sphinx/ext/autodoc/typehints.py +++ b/sphinx/ext/autodoc/typehints.py @@ -115,7 +115,15 @@ def modify_field_list(node: nodes.field_list, annotations: Dict[str, str], if name == 'return': continue - arg = arguments.get(name, {}) + 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) @@ -167,13 +175,19 @@ def augment_descriptions_with_types( has_type.add('return') # Add 'type' for parameters with a description but no declared type. - for name in annotations: + 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('', annotations[name])) + field += nodes.field_body('', nodes.paragraph('', annotation)) node += field # Add 'rtype' if 'return' is present and 'rtype' isn't. diff --git a/tests/roots/test-ext-autodoc/target/typehints.py b/tests/roots/test-ext-autodoc/target/typehints.py index 6b0d6142c..4acfc8911 100644 --- a/tests/roots/test-ext-autodoc/target/typehints.py +++ b/tests/roots/test-ext-autodoc/target/typehints.py @@ -94,8 +94,10 @@ def missing_attr(c, class _ClassWithDocumentedInit: """Class docstring.""" - def __init__(self, x: int) -> None: + def __init__(self, x: int, *args: int, **kwargs: int) -> None: """Init docstring. :param x: Some integer + :param args: Some integer + :param kwargs: Some integer """ diff --git a/tests/roots/test-ext-napoleon/conf.py b/tests/roots/test-ext-napoleon/conf.py new file mode 100644 index 000000000..502fb5a9d --- /dev/null +++ b/tests/roots/test-ext-napoleon/conf.py @@ -0,0 +1,5 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath('.')) +extensions = ['sphinx.ext.napoleon'] diff --git a/tests/roots/test-ext-napoleon/index.rst b/tests/roots/test-ext-napoleon/index.rst new file mode 100644 index 000000000..4c013b75b --- /dev/null +++ b/tests/roots/test-ext-napoleon/index.rst @@ -0,0 +1,6 @@ +test-ext-napoleon +================= + +.. toctree:: + + typehints diff --git a/tests/roots/test-ext-napoleon/mypackage/__init__.py b/tests/roots/test-ext-napoleon/mypackage/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/roots/test-ext-napoleon/mypackage/typehints.py b/tests/roots/test-ext-napoleon/mypackage/typehints.py new file mode 100644 index 000000000..526b78e40 --- /dev/null +++ b/tests/roots/test-ext-napoleon/mypackage/typehints.py @@ -0,0 +1,11 @@ +def hello(x: int, *args: int, **kwargs: int) -> None: + """ + Parameters + ---------- + x + X + *args + Additional arguments. + **kwargs + Extra arguments. + """ diff --git a/tests/roots/test-ext-napoleon/typehints.rst b/tests/roots/test-ext-napoleon/typehints.rst new file mode 100644 index 000000000..43c61f6d2 --- /dev/null +++ b/tests/roots/test-ext-napoleon/typehints.rst @@ -0,0 +1,5 @@ +typehints +========= + +.. automodule:: mypackage.typehints + :members: diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index c075a8e2d..0011f450b 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -1034,19 +1034,27 @@ def test_autodoc_typehints_description_with_documented_init(app): ) app.build() context = (app.outdir / 'index.txt').read_text(encoding='utf8') - assert ('class target.typehints._ClassWithDocumentedInit(x)\n' + assert ('class target.typehints._ClassWithDocumentedInit(x, *args, **kwargs)\n' '\n' ' Class docstring.\n' '\n' ' Parameters:\n' - ' **x** (*int*) --\n' + ' * **x** (*int*) --\n' '\n' - ' __init__(x)\n' + ' * **args** (*int*) --\n' + '\n' + ' * **kwargs** (*int*) --\n' + '\n' + ' __init__(x, *args, **kwargs)\n' '\n' ' Init docstring.\n' '\n' ' Parameters:\n' - ' **x** (*int*) -- Some integer\n' + ' * **x** (*int*) -- Some integer\n' + '\n' + ' * **args** (*int*) -- Some integer\n' + '\n' + ' * **kwargs** (*int*) -- Some integer\n' '\n' ' Return type:\n' ' None\n' == context) @@ -1063,16 +1071,20 @@ def test_autodoc_typehints_description_with_documented_init_no_undoc(app): ) app.build() context = (app.outdir / 'index.txt').read_text(encoding='utf8') - assert ('class target.typehints._ClassWithDocumentedInit(x)\n' + assert ('class target.typehints._ClassWithDocumentedInit(x, *args, **kwargs)\n' '\n' ' Class docstring.\n' '\n' - ' __init__(x)\n' + ' __init__(x, *args, **kwargs)\n' '\n' ' Init docstring.\n' '\n' ' Parameters:\n' - ' **x** (*int*) -- Some integer\n' == context) + ' * **x** (*int*) -- Some integer\n' + '\n' + ' * **args** (*int*) -- Some integer\n' + '\n' + ' * **kwargs** (*int*) -- Some integer\n' == context) @pytest.mark.sphinx('text', testroot='ext-autodoc', @@ -1089,16 +1101,20 @@ def test_autodoc_typehints_description_with_documented_init_no_undoc_doc_rtype(a ) app.build() context = (app.outdir / 'index.txt').read_text(encoding='utf8') - assert ('class target.typehints._ClassWithDocumentedInit(x)\n' + assert ('class target.typehints._ClassWithDocumentedInit(x, *args, **kwargs)\n' '\n' ' Class docstring.\n' '\n' - ' __init__(x)\n' + ' __init__(x, *args, **kwargs)\n' '\n' ' Init docstring.\n' '\n' ' Parameters:\n' - ' **x** (*int*) -- Some integer\n' == context) + ' * **x** (*int*) -- Some integer\n' + '\n' + ' * **args** (*int*) -- Some integer\n' + '\n' + ' * **kwargs** (*int*) -- Some integer\n' == context) @pytest.mark.sphinx('text', testroot='ext-autodoc', diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_ext_napoleon_docstring.py index bdf50f516..6db897932 100644 --- a/tests/test_ext_napoleon_docstring.py +++ b/tests/test_ext_napoleon_docstring.py @@ -2593,3 +2593,48 @@ Sample class with PEP 526 annotations and numpy docstring """ print(actual) assert expected == actual + + +@pytest.mark.sphinx('text', testroot='ext-napoleon', + confoverrides={'autodoc_typehints': 'description', + 'autodoc_typehints_description_target': 'all'}) +def test_napoleon_and_autodoc_typehints_description_all(app, status, warning): + app.build() + content = (app.outdir / 'typehints.txt').read_text(encoding='utf-8') + assert content == ( + 'typehints\n' + '*********\n' + '\n' + 'mypackage.typehints.hello(x, *args, **kwargs)\n' + '\n' + ' Parameters:\n' + ' * **x** (*int*) -- X\n' + '\n' + ' * ***args** (*int*) -- Additional arguments.\n' + '\n' + ' * ****kwargs** (*int*) -- Extra arguments.\n' + '\n' + ' Return type:\n' + ' None\n' + ) + + +@pytest.mark.sphinx('text', testroot='ext-napoleon', + confoverrides={'autodoc_typehints': 'description', + 'autodoc_typehints_description_target': 'documented_params'}) +def test_napoleon_and_autodoc_typehints_description_documented_params(app, status, warning): + app.build() + content = (app.outdir / 'typehints.txt').read_text(encoding='utf-8') + assert content == ( + 'typehints\n' + '*********\n' + '\n' + 'mypackage.typehints.hello(x, *args, **kwargs)\n' + '\n' + ' Parameters:\n' + ' * **x** (*int*) -- X\n' + '\n' + ' * ***args** (*int*) -- Additional arguments.\n' + '\n' + ' * ****kwargs** (*int*) -- Extra arguments.\n' + )