Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ dynamic = [
"version",
]
dependencies = [
"sphinx>=8.1.3",
"sphinx>=8.2",
]
optional-dependencies.docs = [
"furo>=2024.8.6",
Expand Down
31 changes: 20 additions & 11 deletions src/sphinx_autodoc_typehints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,11 @@ def get_annotation_args(annotation: Any, module: str, class_name: str) -> tuple[
return () if len(result) == 1 and result[0] == () else result # type: ignore[misc]


def format_internal_tuple(t: tuple[Any, ...], config: Config) -> str:
def format_internal_tuple(t: tuple[Any, ...], config: Config, *, short_literals: bool = False) -> str:
# An annotation can be a tuple, e.g., for numpy.typing:
# In this case, format_annotation receives:
# This solution should hopefully be general for *any* type that allows tuples in annotations
fmt = [format_annotation(a, config) for a in t]
fmt = [format_annotation(a, config, short_literals=short_literals) for a in t]
if len(fmt) == 0:
return "()"
if len(fmt) == 1:
Expand All @@ -196,12 +196,13 @@ def fixup_module_name(config: Config, module: str) -> str:
return module


def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PLR0911, PLR0912, PLR0915, PLR0914
def format_annotation(annotation: Any, config: Config, *, short_literals: bool = False) -> str: # noqa: C901, PLR0911, PLR0912, PLR0915, PLR0914
"""
Format the annotation.

:param annotation:
:param config:
:param short_literals: Render :py:class:`Literals` in PEP 604 style (``|``).
:return:
"""
typehints_formatter: Callable[..., str] | None = getattr(config, "typehints_formatter", None)
Expand All @@ -222,7 +223,7 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PL
return format_internal_tuple(annotation, config)

if isinstance(annotation, TypeAliasForwardRef):
return str(annotation)
return annotation.name

try:
module = get_annotation_module(annotation)
Expand Down Expand Up @@ -254,7 +255,7 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PL
params = {k: getattr(annotation, f"__{k}__") for k in ("bound", "covariant", "contravariant")}
params = {k: v for k, v in params.items() if v}
if "bound" in params:
params["bound"] = f" {format_annotation(params['bound'], config)}"
params["bound"] = f" {format_annotation(params['bound'], config, short_literals=short_literals)}"
args_format = f"\\(``{annotation.__name__}``{', {}' if args else ''}"
if params:
args_format += "".join(f", {k}={v}" for k, v in params.items())
Expand All @@ -275,20 +276,22 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PL
args_format = f"\\[:py:data:`{prefix}typing.Union`\\[{{}}]]"
args = tuple(x for x in args if x is not type(None))
elif full_name in {"typing.Callable", "collections.abc.Callable"} and args and args[0] is not ...:
fmt = [format_annotation(arg, config) for arg in args]
fmt = [format_annotation(arg, config, short_literals=short_literals) for arg in args]
formatted_args = f"\\[\\[{', '.join(fmt[:-1])}], {fmt[-1]}]"
elif full_name == "typing.Literal":
if short_literals:
return f"\\{' | '.join(f'``{arg!r}``' for arg in args)}"
formatted_args = f"\\[{', '.join(f'``{arg!r}``' for arg in args)}]"
elif is_bars_union:
return " | ".join([format_annotation(arg, config) for arg in args])
return " | ".join([format_annotation(arg, config, short_literals=short_literals) for arg in args])

if args and not formatted_args:
try:
iter(args)
except TypeError:
fmt = [format_annotation(args, config)]
fmt = [format_annotation(args, config, short_literals=short_literals)]
else:
fmt = [format_annotation(arg, config) for arg in args]
fmt = [format_annotation(arg, config, short_literals=short_literals) for arg in args]
formatted_args = args_format.format(", ".join(fmt))

escape = "\\ " if formatted_args else ""
Expand Down Expand Up @@ -783,7 +786,10 @@ def _inject_signature(
if annotation is None:
type_annotation = f":type {arg_name}: "
else:
formatted_annotation = add_type_css_class(format_annotation(annotation, app.config))
short_literals = app.config.python_display_short_literal_types
formatted_annotation = add_type_css_class(
format_annotation(annotation, app.config, short_literals=short_literals)
)
type_annotation = f":type {arg_name}: {formatted_annotation}"

if app.config.typehints_defaults:
Expand Down Expand Up @@ -923,7 +929,10 @@ def _inject_rtype( # noqa: PLR0913, PLR0917
if not app.config.typehints_use_rtype and r.found_return and " -- " in lines[insert_index]:
return

formatted_annotation = add_type_css_class(format_annotation(type_hints["return"], app.config))
short_literals = app.config.python_display_short_literal_types
formatted_annotation = add_type_css_class(
format_annotation(type_hints["return"], app.config, short_literals=short_literals)
)

if r.found_param and insert_index < len(lines) and lines[insert_index].strip():
insert_index -= 1
Expand Down
4 changes: 2 additions & 2 deletions src/sphinx_autodoc_typehints/attributes_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@
orig_handle_signature = PyAttribute.handle_signature


def _stringify_annotation(app: Sphinx, annotation: Any, mode: str = "") -> str: # noqa: ARG001
def _stringify_annotation(app: Sphinx, annotation: Any, *args: Any, short_literals: bool = False, **kwargs: Any) -> str: # noqa: ARG001
# Format the annotation with sphinx-autodoc-typehints and inject our magic prefix to tell our patched
# PyAttribute.handle_signature to treat it as rst.
from . import format_annotation # noqa: PLC0415

return TYPE_IS_RST_LABEL + format_annotation(annotation, app.config)
return TYPE_IS_RST_LABEL + format_annotation(annotation, app.config, short_literals=short_literals)


def patch_attribute_documenter(app: Sphinx) -> None:
Expand Down
56 changes: 55 additions & 1 deletion tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from typing import ( # no type comments
TYPE_CHECKING,
Any,
Literal,
NewType,
Optional,
TypeVar,
Expand Down Expand Up @@ -661,6 +662,59 @@ def func_with_overload(a: Union[int, str], b: Union[int, str]) -> None:
"""


@expected(
"""\
mod.func_literals_long_format(a, b)

A docstring.

Parameters:
* **a** ("Literal"["'arg1'", "'arg2'"]) -- Argument that can
take either of two literal values.

* **b** ("Literal"["'arg1'", "'arg2'"]) -- Argument that can
take either of two literal values.

Return type:
"None"
""",
)
def func_literals_long_format(a: Literal["arg1", "arg2"], b: Literal["arg1", "arg2"]) -> None:
"""
A docstring.

:param a: Argument that can take either of two literal values.
:param b: Argument that can take either of two literal values.
"""


@expected(
"""\
mod.func_literals_short_format(a, b)

A docstring.

Parameters:
* **a** ("'arg1'" | "'arg2'") -- Argument that can take either
of two literal values.

* **b** ("'arg1'" | "'arg2'") -- Argument that can take either
of two literal values.

Return type:
"None"
""",
python_display_short_literal_types=True,
)
def func_literals_short_format(a: Literal["arg1", "arg2"], b: Literal["arg1", "arg2"]) -> None:
"""
A docstring.

:param a: Argument that can take either of two literal values.
:param b: Argument that can take either of two literal values.
"""


@expected(
"""\
class mod.TestClassAttributeDocs
Expand Down Expand Up @@ -1386,7 +1440,7 @@ def has_doctest1() -> None:
Unformatted = TypeVar("Unformatted")


@warns("cannot cache unpickable configuration value: 'typehints_formatter'")
@warns("cannot cache unpickleable configuration value: 'typehints_formatter'")
@expected(
"""
mod.typehints_formatter_applied_to_signature(param: Formatted) -> Formatted
Expand Down
Loading