From 92fcac321be6598e22877465b3d60730aa24f1f1 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 15 May 2022 19:05:17 +0900 Subject: [PATCH 1/2] test: Add *args and **kwargs to testcase of target.typehints --- .../test-ext-autodoc/target/typehints.py | 4 ++- tests/test_ext_autodoc_configs.py | 36 +++++++++++++------ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/tests/roots/test-ext-autodoc/target/typehints.py b/tests/roots/test-ext-autodoc/target/typehints.py index 6b0d6142c1c..4acfc891125 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/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index fdb82b1b928..12ec0e022b7 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -1034,22 +1034,30 @@ 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' + ' * **args** (*int*) --\n' + '\n' + ' * **kwargs** (*int*) --\n' '\n' ' Return type:\n' ' None\n' '\n' - ' __init__(x)\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) @@ -1066,16 +1074,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', @@ -1092,16 +1104,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', From 06a9139308c15058087040ddff0f2ca9a4335360 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 15 May 2022 20:43:04 +0900 Subject: [PATCH 2/2] Fix #9648: autodoc: *args and **kwargs entries are duplicated In basic usage of autodoc (docstring), `args` and `kwargs` arguments are marked up without stars. But numpydoc style recommends to mark them up with stars. This adds support for starred arguments in docstrings to `autodoc_typehints` feature. --- CHANGES | 3 ++ sphinx/ext/autodoc/typehints.py | 20 +++++++-- tests/roots/test-ext-napoleon/conf.py | 5 +++ tests/roots/test-ext-napoleon/index.rst | 6 +++ .../test-ext-napoleon/mypackage/__init__.py | 0 .../test-ext-napoleon/mypackage/typehints.py | 11 +++++ tests/roots/test-ext-napoleon/typehints.rst | 5 +++ tests/test_ext_napoleon_docstring.py | 45 +++++++++++++++++++ 8 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 tests/roots/test-ext-napoleon/conf.py create mode 100644 tests/roots/test-ext-napoleon/index.rst create mode 100644 tests/roots/test-ext-napoleon/mypackage/__init__.py create mode 100644 tests/roots/test-ext-napoleon/mypackage/typehints.py create mode 100644 tests/roots/test-ext-napoleon/typehints.rst diff --git a/CHANGES b/CHANGES index 424a12eaae6..dfbd6bd5364 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,9 @@ Features added Bugs fixed ---------- +* #9648: autodoc: ``*args`` and ``**kwargs`` entries are duplicated when + ``autodoc_typehints="description"`` + Testing -------- diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py index 06768168eb7..b18107c8479 100644 --- a/sphinx/ext/autodoc/typehints.py +++ b/sphinx/ext/autodoc/typehints.py @@ -111,7 +111,15 @@ def modify_field_list(node: nodes.field_list, annotations: Dict[str, str]) -> No 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) @@ -159,13 +167,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-napoleon/conf.py b/tests/roots/test-ext-napoleon/conf.py new file mode 100644 index 00000000000..502fb5a9de9 --- /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 00000000000..4c013b75b2e --- /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 00000000000..e69de29bb2d 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 00000000000..526b78e40bb --- /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 00000000000..43c61f6d240 --- /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_napoleon_docstring.py b/tests/test_ext_napoleon_docstring.py index bdf50f516db..6db8979320a 100644 --- a/tests/test_ext_napoleon_docstring.py +++ b/tests/test_ext_napoleon_docstring.py @@ -2593,3 +2593,48 @@ def test_pep526_annotations(self): """ 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' + )