Skip to content
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
37 changes: 29 additions & 8 deletions src/sphinx_autodoc_typehints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import inspect
import re
import types
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any, TypeVar

from docutils import nodes
Expand Down Expand Up @@ -40,11 +41,12 @@
from .version import __version__

if TYPE_CHECKING:
from collections.abc import Callable
from collections.abc import Callable, Iterator

from docutils.nodes import Node
from docutils.parsers.rst import states
from sphinx.application import Sphinx
from sphinx.config import Config
from sphinx.environment import BuildEnvironment
from sphinx.ext.autodoc import Options

Expand Down Expand Up @@ -196,17 +198,36 @@ def process_docstring( # ruff:ignore[too-many-arguments, too-many-positional-ar
for param, hint in type_hints.items():
if id(hint) in eager_aliases:
type_hints[param] = eager_aliases[id(hint)]
app.config._annotation_globals = getattr(obj, "__globals__", {}) # ruff:ignore[private-member-access]
app.config._typehints_env = env # ruff:ignore[private-member-access]
app.config._typehints_module_prefix = module_prefix # ruff:ignore[private-member-access]
try:
with _annotation_state(
app.config,
_annotation_globals=getattr(obj, "__globals__", {}),
_typehints_env=env,
_typehints_module_prefix=module_prefix,
):
has_overloads = _inject_overload_signatures(app, what, name, obj, lines)
_inject_types_to_docstring(type_hints, signature, original_obj, app, what, name, lines, has_overloads)
_inject_ivar_types(ivar_annotations, app, lines)


@contextmanager
def _annotation_state(config: Config, **values: Any) -> Iterator[None]:
"""
Publish the state format_annotation reads off the config, restoring what was there.

Documenting an object can re-enter this handler for another one, and deleting the attributes
on the way out of the inner call then broke the outer call's teardown (#750).
"""
previous = {name: getattr(config, name) for name in values if hasattr(config, name)}
for name, value in values.items():
setattr(config, name, value)
try:
yield
finally:
del app.config._annotation_globals # ruff:ignore[private-member-access]
del app.config._typehints_env # ruff:ignore[private-member-access]
del app.config._typehints_module_prefix # ruff:ignore[private-member-access]
for name in values:
if name in previous:
setattr(config, name, previous[name])
else:
delattr(config, name)


def _maybe_inject_descriptor_type(app: Sphinx, what: str, obj: Any, lines: list[str]) -> None:
Expand Down
28 changes: 28 additions & 0 deletions tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,3 +430,31 @@ def test_setup_returns_version() -> None:
assert result["version"] == __version__
assert result["parallel_read_safe"] is True
assert result["parallel_write_safe"] is True


def test_process_docstring_reentrant_call_keeps_outer_state() -> None:
"""A formatter documenting a second object must not break the outer teardown (#750)."""

def inner(flag: str) -> str: ...

def outer(flag: bool) -> bool: ...

app = make_docstring_app()

def formatter(annotation: Any, config: Config) -> None: # ruff:ignore[unused-function-argument]
if annotation is bool:
process_docstring(app, "function", "test.inner", inner, None, ["Inner.", "", ":return: it"])

app.config.typehints_formatter = formatter
lines = ["Outer.", "", ":param flag: the flag", ":return: it"]
process_docstring(app, "function", "test.outer", outer, None, lines)
bool_type = ":sphinx_autodoc_typehints_type:`\\:py\\:class\\:\\`bool\\``"
assert lines == [
"Outer.",
"",
f":type flag: {bool_type}",
":param flag: the flag",
"",
f":rtype: {bool_type}",
":return: it",
]