Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for Issue #384: typehints_defaults = "braces-after" fails for a multiline :param: entry #464

Merged
merged 6 commits into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions src/sphinx_autodoc_typehints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,7 @@ def _inject_types_to_docstring( # noqa: PLR0913, PLR0917
_inject_rtype(type_hints, original_obj, app, what, name, lines)


def _inject_signature( # noqa: C901
def _inject_signature(
type_hints: dict[str, Any],
signature: inspect.Signature,
app: Sphinx,
Expand Down Expand Up @@ -754,14 +754,33 @@ def _inject_signature( # noqa: C901
if app.config.typehints_defaults:
formatted_default = format_default(app, default, annotation is not None)
if formatted_default:
if app.config.typehints_defaults.endswith("after"):
lines[insert_index] += formatted_default
else: # add to last param doc line
type_annotation += formatted_default
type_annotation = _append_default(app, lines, insert_index, type_annotation, formatted_default)

lines.insert(insert_index, type_annotation)


def _append_default(
app: Sphinx, lines: list[str], insert_index: int, type_annotation: str, formatted_default: str
) -> str:
if app.config.typehints_defaults.endswith("after"):
# advance the index to the end of the :param: paragraphs
# (terminated by a line with no indentation)
# append default to the last nonempty line
nlines = len(lines)
next_index = insert_index + 1
append_index = insert_index # last nonempty line
while next_index < nlines and (not lines[next_index] or lines[next_index].startswith(" ")):
if lines[next_index]:
append_index = next_index
next_index += 1
lines[append_index] += formatted_default

else: # add to last param doc line
type_annotation += formatted_default

return type_annotation


@dataclass
class InsertIndexInfo:
insert_index: int
Expand Down
119 changes: 119 additions & 0 deletions tests/test_integration_issue_384.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from __future__ import annotations

import re
import sys
from pathlib import Path
from textwrap import dedent, indent
from typing import TYPE_CHECKING, Any, Callable, NewType, TypeVar # no type comments

import pytest

if TYPE_CHECKING:
from io import StringIO

from sphinx.testing.util import SphinxTestApp

T = TypeVar("T")
W = NewType("W", str)


def expected(expected: str, **options: dict[str, Any]) -> Callable[[T], T]:
def dec(val: T) -> T:
val.EXPECTED = expected
val.OPTIONS = options
return val

return dec


def warns(pattern: str) -> Callable[[T], T]:
def dec(val: T) -> T:
val.WARNING = pattern
return val

return dec


@expected(
"""\
mod.function(x=5, y=10, z=15)

Function docstring.

Parameters:
* **x** ("int") -- optional specifier line 2 (default: "5")

* **y** ("int") --

another optional line 4

second paragraph for y (default: "10")

* **z** ("int") -- yet another optional s line 6 (default: "15")

Returns:
something

Return type:
bytes

""",
)
def function(x: int = 5, y: int = 10, z: int = 15) -> str: # noqa: ARG001
"""
Function docstring.

:param x: optional specifier
line 2
:param y: another optional
line 4

second paragraph for y

:param z: yet another optional s
line 6

:return: something
:rtype: bytes
"""


# Config settings for each test run.
# Config Name: Sphinx Options as Dict.
configs = {"default_conf": {"typehints_defaults": "braces-after"}}


@pytest.mark.parametrize("val", [x for x in globals().values() if hasattr(x, "EXPECTED")])
@pytest.mark.parametrize("conf_run", list(configs.keys()))
@pytest.mark.sphinx("text", testroot="integration")
def test_integration(
app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, val: Any, conf_run: str
) -> None:
template = ".. autofunction:: mod.{}"

(Path(app.srcdir) / "index.rst").write_text(template.format(val.__name__))
app.config.__dict__.update(configs[conf_run])
app.config.__dict__.update(val.OPTIONS)
monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__])
app.build()
assert "build succeeded" in status.getvalue() # Build succeeded

regexp = getattr(val, "WARNING", None)
value = warning.getvalue().strip()
if regexp:
msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
assert re.search(regexp, value), msg
else:
assert not value

result = (Path(app.srcdir) / "_build/text/index.txt").read_text()

expected = val.EXPECTED
if sys.version_info < (3, 10):
expected = expected.replace("NewType", "NewType()")
try:
assert result.strip() == dedent(expected).strip()
except Exception:
indented = indent(f'"""\n{result}\n"""', " " * 4)
print(f"@expected(\n{indented}\n)\n") # noqa: T201
raise
Loading