Skip to content

Commit

Permalink
Fix for Issue #384: typehints_defaults = "braces-after" fails for a m…
Browse files Browse the repository at this point in the history
…ultiline `: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>
  • Loading branch information
3 people authored Jun 22, 2024
1 parent 0435d07 commit ea99f28
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 5 deletions.
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

0 comments on commit ea99f28

Please sign in to comment.