Skip to content

Support Sphinx 8.2.0 - drop 3.10 support because Sphinx does #525

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

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
1 change: 0 additions & 1 deletion .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ jobs:
- "3.13"
- "3.12"
- "3.11"
- "3.10"
- type
- dev
- pkg_meta
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ repos:
hooks:
- id: prettier
additional_dependencies:
- prettier@3.4.2
- prettier@3.5.1
- "@prettier/plugin-xml@3.4.1"
- repo: meta
hooks:
Expand Down
11 changes: 5 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,14 @@ maintainers = [
authors = [
{ name = "Bernát Gábor", email = "gaborjbernat@gmail.com" },
]
requires-python = ">=3.10"
requires-python = ">=3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Framework :: Sphinx :: Extension",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
Expand All @@ -41,16 +40,16 @@ dynamic = [
"version",
]
dependencies = [
"sphinx>=8.1.3",
"sphinx>=8.2",
]
optional-dependencies.docs = [
"furo>=2024.8.6",
]
optional-dependencies.testing = [
"covdefaults>=2.3",
"coverage>=7.6.10",
"coverage>=7.6.12",
"defusedxml>=0.7.1", # required by sphinx.testing
"diff-cover>=9.2.1",
"diff-cover>=9.2.3",
"pytest>=8.3.4",
"pytest-cov>=6",
"sphobjinv>=2.3.1.2",
Expand Down Expand Up @@ -142,7 +141,7 @@ run.plugins = [
]

[tool.mypy]
python_version = "3.10"
python_version = "3.11"
strict = true
exclude = "^(.*/roots/.*)|(tests/test_integration.*.py)$"
overrides = [
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
5 changes: 2 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import re
import shutil
import sys
from contextlib import suppress
from pathlib import Path
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -36,11 +37,9 @@ def _remove_sphinx_projects(sphinx_test_tempdir: Path) -> None:
# the temporary directory area.
# See https://github.com/sphinx-doc/sphinx/issues/4040
for entry in sphinx_test_tempdir.iterdir():
try:
with suppress(PermissionError):
if entry.is_dir() and Path(entry, "_build").exists():
shutil.rmtree(str(entry))
except PermissionError: # noqa: PERF203
pass


@pytest.fixture
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
13 changes: 1 addition & 12 deletions tests/test_sphinx_autodoc_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,17 +554,6 @@ class dummy_module.DataClass(x)
assert contents == expected_contents


def maybe_fix_py310(expected_contents: str) -> str:
if sys.version_info >= (3, 11):
return expected_contents

for old, new in [
('"str" | "None"', '"Optional"["str"]'),
]:
expected_contents = expected_contents.replace(old, new)
return expected_contents


@pytest.mark.sphinx("text", testroot="dummy")
@patch("sphinx.writers.text.MAXWIDTH", 2000)
def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) -> None:
Expand Down Expand Up @@ -595,7 +584,7 @@ def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO)
"str"
"""
expected_contents = dedent(expected_contents)
expected_contents = maybe_fix_py310(dedent(expected_contents))
expected_contents = dedent(expected_contents)
assert contents == expected_contents


Expand Down
11 changes: 5 additions & 6 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
[tox]
requires =
tox>=4.23.2
tox-uv>=1.16.2
tox>=4.24.1
tox-uv>=1.24
env_list =
fix
3.13
3.12
3.11
3.10
type
pkg_meta
skip_missing_interpreters = true
Expand Down Expand Up @@ -45,7 +44,7 @@ commands =
[testenv:type]
description = run type check on code base
deps =
mypy==1.14
mypy==1.15
types-docutils>=0.21.0.20241128
commands =
mypy src
Expand All @@ -56,8 +55,8 @@ description = check that the long description is valid
skip_install = true
deps =
check-wheel-contents>=0.6.1
twine>=6.0.1
uv>=0.5.11
twine>=6.1
uv>=0.6.1
commands =
uv build --sdist --wheel --out-dir {env_tmp_dir} .
twine check {env_tmp_dir}{/}*
Expand Down
Loading