Skip to content

Commit a03875e

Browse files
authored
🐛 fix(docstring): keep types on params with a trailing underscore (#714)
1 parent 87ae703 commit a03875e

3 files changed

Lines changed: 121 additions & 3 deletions

File tree

src/sphinx_autodoc_typehints/__init__.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -325,9 +325,9 @@ def _inject_arg_signature( # noqa: PLR0913, PLR0917
325325
doc_description = _extract_doc_description(annotation) if annotation is not None else None
326326

327327
if arg_name.endswith("_"):
328-
arg_name = f"{arg_name[:-1]}\\_"
329-
330-
insert_index = fmt.find_param(lines, arg_name)
328+
arg_name, insert_index = _find_trailing_underscore_param(lines, arg_name, fmt)
329+
else:
330+
insert_index = fmt.find_param(lines, arg_name)
331331

332332
if (
333333
insert_index is not None
@@ -376,6 +376,21 @@ def _inject_arg_signature( # noqa: PLR0913, PLR0917
376376
lines.insert(insert_index, type_annotation)
377377

378378

379+
def _find_trailing_underscore_param(lines: list[str], arg_name: str, fmt: Any) -> tuple[str, int | None]:
380+
escaped_name = f"{arg_name[:-1]}\\_"
381+
if (insert_index := fmt.find_param(lines, escaped_name)) is not None:
382+
return escaped_name, insert_index
383+
if (insert_index := fmt.find_param(lines, arg_name)) is None:
384+
return escaped_name, None
385+
# napoleon only escapes the trailing underscore when strip_signature_backslash is on, and docutils
386+
# swallows an unescaped one as reference markup — see issue #708
387+
rewritten = lines[insert_index].replace(f" {arg_name}:", f" {escaped_name}:", 1)
388+
if rewritten == lines[insert_index]:
389+
return arg_name, insert_index
390+
lines[insert_index] = rewritten
391+
return escaped_name, insert_index
392+
393+
379394
def _remove_preexisting_type(lines: list[str], preexisting_line: str) -> int:
380395
idx = lines.index(preexisting_line)
381396
end = idx + 1

tests/test_init.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from __future__ import annotations
22

33
import inspect
4+
from typing import Any
45
from unittest.mock import MagicMock, create_autospec, patch
56

7+
import pytest
68
from conftest import make_docstring_app, make_sig_app
79
from sphinx.application import Sphinx
810
from sphinx.config import Config
@@ -309,6 +311,52 @@ def func(fp: int) -> None: ...
309311
assert "int" in type_lines[0]
310312

311313

314+
def _trailing_underscore_func(lambda_: float) -> None: ...
315+
316+
317+
def _trailing_underscore_starred_func(*args_: float) -> None: ...
318+
319+
320+
def _trailing_underscore_undocumented_func(x_: float) -> None: ...
321+
322+
323+
@pytest.mark.parametrize(
324+
("func", "lines", "expected_param_line", "expected_type_name"),
325+
[
326+
pytest.param(
327+
_trailing_underscore_func,
328+
[":param lambda_: description"],
329+
":param lambda\\_: description",
330+
"lambda\\_",
331+
id="unescaped-line-rewritten",
332+
),
333+
pytest.param(
334+
_trailing_underscore_starred_func,
335+
[":param \\*args_: description"],
336+
":param \\*args_: description",
337+
"args_",
338+
id="starred-line-kept",
339+
),
340+
pytest.param(
341+
_trailing_underscore_undocumented_func,
342+
[],
343+
":param x\\_:",
344+
"x\\_",
345+
id="undocumented-escaped",
346+
),
347+
],
348+
)
349+
def test_process_docstring_trailing_underscore_param(
350+
func: Any, lines: list[str], expected_param_line: str, expected_type_name: str
351+
) -> None:
352+
"""napoleon emits ``:param lambda_:`` unescaped by default; the param line must be rewritten to
353+
the escaped form and the type attached, except for forms that cannot be rewritten (issue #708)."""
354+
app = make_docstring_app(typehints_document_rtype=False, always_document_param_types=True)
355+
process_docstring(app, "function", "test.func", func, None, lines)
356+
assert expected_param_line in lines
357+
assert any(line.startswith(":type ") and expected_type_name in line and "float" in line for line in lines)
358+
359+
312360
def test_process_signature_annotations_name_error() -> None:
313361
"""PEP 649 lazy annotations raising NameError must not propagate from process_signature."""
314362

tests/test_integration.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,61 @@ def function(x: bool, y: int, z_: Optional[str] = None) -> str:
304304
"""
305305

306306

307+
@expected(
308+
"""\
309+
mod.function_with_trailing_underscore_numpy(a, lambda_=None)
310+
311+
Do nothing.
312+
313+
Parameters:
314+
* **a** ("int") -- Description for a.
315+
316+
* **lambda_** ("float" | "None", default: "None") -- Description
317+
for lambda.
318+
319+
Return type:
320+
"None"
321+
""",
322+
typehints_defaults="comma",
323+
)
324+
def function_with_trailing_underscore_numpy(a: int, lambda_: float | None = None) -> None:
325+
"""Do nothing.
326+
327+
Parameters
328+
----------
329+
a
330+
Description for a.
331+
lambda_
332+
Description for lambda.
333+
"""
334+
335+
336+
@expected(
337+
"""\
338+
mod.function_with_trailing_underscore_google(a, lambda_=None)
339+
340+
Do nothing.
341+
342+
Parameters:
343+
* **a** ("int") -- Description for a.
344+
345+
* **lambda_** ("float" | "None", default: "None") -- Description
346+
for lambda.
347+
348+
Return type:
349+
"None"
350+
""",
351+
typehints_defaults="comma",
352+
)
353+
def function_with_trailing_underscore_google(a: int, lambda_: float | None = None) -> None:
354+
"""Do nothing.
355+
356+
Args:
357+
a: Description for a.
358+
lambda_: Description for lambda.
359+
"""
360+
361+
307362
@expected(
308363
"""\
309364
mod.function_with_starred_documentation_param_names(*args, **kwargs)

0 commit comments

Comments
 (0)