Skip to content

Commit ea99f28

Browse files
tikuma-lsuhscpre-commit-ci[bot]gaborbernat
authored
Fix for Issue #384: typehints_defaults = "braces-after" fails for a multiline :param: entry (#464)
* wip * wip: unexpected unindent error * conforming to pre-commit * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * use descriptive index variable names --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bernát Gábor <gaborjbernat@gmail.com>
1 parent 0435d07 commit ea99f28

File tree

2 files changed

+143
-5
lines changed

2 files changed

+143
-5
lines changed

src/sphinx_autodoc_typehints/__init__.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,7 @@ def _inject_types_to_docstring( # noqa: PLR0913, PLR0917
716716
_inject_rtype(type_hints, original_obj, app, what, name, lines)
717717

718718

719-
def _inject_signature( # noqa: C901
719+
def _inject_signature(
720720
type_hints: dict[str, Any],
721721
signature: inspect.Signature,
722722
app: Sphinx,
@@ -754,14 +754,33 @@ def _inject_signature( # noqa: C901
754754
if app.config.typehints_defaults:
755755
formatted_default = format_default(app, default, annotation is not None)
756756
if formatted_default:
757-
if app.config.typehints_defaults.endswith("after"):
758-
lines[insert_index] += formatted_default
759-
else: # add to last param doc line
760-
type_annotation += formatted_default
757+
type_annotation = _append_default(app, lines, insert_index, type_annotation, formatted_default)
761758

762759
lines.insert(insert_index, type_annotation)
763760

764761

762+
def _append_default(
763+
app: Sphinx, lines: list[str], insert_index: int, type_annotation: str, formatted_default: str
764+
) -> str:
765+
if app.config.typehints_defaults.endswith("after"):
766+
# advance the index to the end of the :param: paragraphs
767+
# (terminated by a line with no indentation)
768+
# append default to the last nonempty line
769+
nlines = len(lines)
770+
next_index = insert_index + 1
771+
append_index = insert_index # last nonempty line
772+
while next_index < nlines and (not lines[next_index] or lines[next_index].startswith(" ")):
773+
if lines[next_index]:
774+
append_index = next_index
775+
next_index += 1
776+
lines[append_index] += formatted_default
777+
778+
else: # add to last param doc line
779+
type_annotation += formatted_default
780+
781+
return type_annotation
782+
783+
765784
@dataclass
766785
class InsertIndexInfo:
767786
insert_index: int

tests/test_integration_issue_384.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
from __future__ import annotations
2+
3+
import re
4+
import sys
5+
from pathlib import Path
6+
from textwrap import dedent, indent
7+
from typing import TYPE_CHECKING, Any, Callable, NewType, TypeVar # no type comments
8+
9+
import pytest
10+
11+
if TYPE_CHECKING:
12+
from io import StringIO
13+
14+
from sphinx.testing.util import SphinxTestApp
15+
16+
T = TypeVar("T")
17+
W = NewType("W", str)
18+
19+
20+
def expected(expected: str, **options: dict[str, Any]) -> Callable[[T], T]:
21+
def dec(val: T) -> T:
22+
val.EXPECTED = expected
23+
val.OPTIONS = options
24+
return val
25+
26+
return dec
27+
28+
29+
def warns(pattern: str) -> Callable[[T], T]:
30+
def dec(val: T) -> T:
31+
val.WARNING = pattern
32+
return val
33+
34+
return dec
35+
36+
37+
@expected(
38+
"""\
39+
mod.function(x=5, y=10, z=15)
40+
41+
Function docstring.
42+
43+
Parameters:
44+
* **x** ("int") -- optional specifier line 2 (default: "5")
45+
46+
* **y** ("int") --
47+
48+
another optional line 4
49+
50+
second paragraph for y (default: "10")
51+
52+
* **z** ("int") -- yet another optional s line 6 (default: "15")
53+
54+
Returns:
55+
something
56+
57+
Return type:
58+
bytes
59+
60+
""",
61+
)
62+
def function(x: int = 5, y: int = 10, z: int = 15) -> str: # noqa: ARG001
63+
"""
64+
Function docstring.
65+
66+
:param x: optional specifier
67+
line 2
68+
:param y: another optional
69+
line 4
70+
71+
second paragraph for y
72+
73+
:param z: yet another optional s
74+
line 6
75+
76+
:return: something
77+
:rtype: bytes
78+
"""
79+
80+
81+
# Config settings for each test run.
82+
# Config Name: Sphinx Options as Dict.
83+
configs = {"default_conf": {"typehints_defaults": "braces-after"}}
84+
85+
86+
@pytest.mark.parametrize("val", [x for x in globals().values() if hasattr(x, "EXPECTED")])
87+
@pytest.mark.parametrize("conf_run", list(configs.keys()))
88+
@pytest.mark.sphinx("text", testroot="integration")
89+
def test_integration(
90+
app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, val: Any, conf_run: str
91+
) -> None:
92+
template = ".. autofunction:: mod.{}"
93+
94+
(Path(app.srcdir) / "index.rst").write_text(template.format(val.__name__))
95+
app.config.__dict__.update(configs[conf_run])
96+
app.config.__dict__.update(val.OPTIONS)
97+
monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__])
98+
app.build()
99+
assert "build succeeded" in status.getvalue() # Build succeeded
100+
101+
regexp = getattr(val, "WARNING", None)
102+
value = warning.getvalue().strip()
103+
if regexp:
104+
msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
105+
assert re.search(regexp, value), msg
106+
else:
107+
assert not value
108+
109+
result = (Path(app.srcdir) / "_build/text/index.txt").read_text()
110+
111+
expected = val.EXPECTED
112+
if sys.version_info < (3, 10):
113+
expected = expected.replace("NewType", "NewType()")
114+
try:
115+
assert result.strip() == dedent(expected).strip()
116+
except Exception:
117+
indented = indent(f'"""\n{result}\n"""', " " * 4)
118+
print(f"@expected(\n{indented}\n)\n") # noqa: T201
119+
raise

0 commit comments

Comments
 (0)