Skip to content

Commit ca5fcc0

Browse files
Support Sphinx 8.2.0 - drop 3.10 support because Sphinx does (#525)
* chore(deps): update dependency sphinx to v8.2.0 * refactor: patch stringify_annotation with arbitrary *args and **kwargs * fix: derive annotation for TypeAliasForwardRef from its name attribute TypeAliasForwardRef.__repr__ was changed in sphinx v8.2.0 to not only yield the name, but also the class name. * test: update warning message "unpickable" was renamed to "unpickleable" in sphinx v8.2.0 * feat: respect configuration option python_display_short_literal_types https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-python_display_short_literal_types * Sphinx 8.2 drops 3.10 support Signed-off-by: Bernát Gábor <[email protected]> --------- Signed-off-by: Bernát Gábor <[email protected]> Co-authored-by: Bernát Gábor <[email protected]>
1 parent f09eb89 commit ca5fcc0

File tree

9 files changed

+91
-43
lines changed

9 files changed

+91
-43
lines changed

.github/workflows/check.yaml

-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ jobs:
2222
- "3.13"
2323
- "3.12"
2424
- "3.11"
25-
- "3.10"
2625
- type
2726
- dev
2827
- pkg_meta

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ repos:
3434
hooks:
3535
- id: prettier
3636
additional_dependencies:
37-
- prettier@3.4.2
37+
- prettier@3.5.1
3838
- "@prettier/[email protected]"
3939
- repo: meta
4040
hooks:

pyproject.toml

+5-6
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,14 @@ maintainers = [
2323
authors = [
2424
{ name = "Bernát Gábor", email = "[email protected]" },
2525
]
26-
requires-python = ">=3.10"
26+
requires-python = ">=3.11"
2727
classifiers = [
2828
"Development Status :: 5 - Production/Stable",
2929
"Framework :: Sphinx :: Extension",
3030
"Intended Audience :: Developers",
3131
"License :: OSI Approved :: MIT License",
3232
"Programming Language :: Python",
3333
"Programming Language :: Python :: 3 :: Only",
34-
"Programming Language :: Python :: 3.10",
3534
"Programming Language :: Python :: 3.11",
3635
"Programming Language :: Python :: 3.12",
3736
"Programming Language :: Python :: 3.13",
@@ -41,16 +40,16 @@ dynamic = [
4140
"version",
4241
]
4342
dependencies = [
44-
"sphinx>=8.1.3",
43+
"sphinx>=8.2",
4544
]
4645
optional-dependencies.docs = [
4746
"furo>=2024.8.6",
4847
]
4948
optional-dependencies.testing = [
5049
"covdefaults>=2.3",
51-
"coverage>=7.6.10",
50+
"coverage>=7.6.12",
5251
"defusedxml>=0.7.1", # required by sphinx.testing
53-
"diff-cover>=9.2.1",
52+
"diff-cover>=9.2.3",
5453
"pytest>=8.3.4",
5554
"pytest-cov>=6",
5655
"sphobjinv>=2.3.1.2",
@@ -142,7 +141,7 @@ run.plugins = [
142141
]
143142

144143
[tool.mypy]
145-
python_version = "3.10"
144+
python_version = "3.11"
146145
strict = true
147146
exclude = "^(.*/roots/.*)|(tests/test_integration.*.py)$"
148147
overrides = [

src/sphinx_autodoc_typehints/__init__.py

+20-11
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,11 @@ def get_annotation_args(annotation: Any, module: str, class_name: str) -> tuple[
172172
return () if len(result) == 1 and result[0] == () else result # type: ignore[misc]
173173

174174

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

198198

199-
def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PLR0911, PLR0912, PLR0915, PLR0914
199+
def format_annotation(annotation: Any, config: Config, *, short_literals: bool = False) -> str: # noqa: C901, PLR0911, PLR0912, PLR0915, PLR0914
200200
"""
201201
Format the annotation.
202202
203203
:param annotation:
204204
:param config:
205+
:param short_literals: Render :py:class:`Literals` in PEP 604 style (``|``).
205206
:return:
206207
"""
207208
typehints_formatter: Callable[..., str] | None = getattr(config, "typehints_formatter", None)
@@ -222,7 +223,7 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PL
222223
return format_internal_tuple(annotation, config)
223224

224225
if isinstance(annotation, TypeAliasForwardRef):
225-
return str(annotation)
226+
return annotation.name
226227

227228
try:
228229
module = get_annotation_module(annotation)
@@ -254,7 +255,7 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PL
254255
params = {k: getattr(annotation, f"__{k}__") for k in ("bound", "covariant", "contravariant")}
255256
params = {k: v for k, v in params.items() if v}
256257
if "bound" in params:
257-
params["bound"] = f" {format_annotation(params['bound'], config)}"
258+
params["bound"] = f" {format_annotation(params['bound'], config, short_literals=short_literals)}"
258259
args_format = f"\\(``{annotation.__name__}``{', {}' if args else ''}"
259260
if params:
260261
args_format += "".join(f", {k}={v}" for k, v in params.items())
@@ -275,20 +276,22 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PL
275276
args_format = f"\\[:py:data:`{prefix}typing.Union`\\[{{}}]]"
276277
args = tuple(x for x in args if x is not type(None))
277278
elif full_name in {"typing.Callable", "collections.abc.Callable"} and args and args[0] is not ...:
278-
fmt = [format_annotation(arg, config) for arg in args]
279+
fmt = [format_annotation(arg, config, short_literals=short_literals) for arg in args]
279280
formatted_args = f"\\[\\[{', '.join(fmt[:-1])}], {fmt[-1]}]"
280281
elif full_name == "typing.Literal":
282+
if short_literals:
283+
return f"\\{' | '.join(f'``{arg!r}``' for arg in args)}"
281284
formatted_args = f"\\[{', '.join(f'``{arg!r}``' for arg in args)}]"
282285
elif is_bars_union:
283-
return " | ".join([format_annotation(arg, config) for arg in args])
286+
return " | ".join([format_annotation(arg, config, short_literals=short_literals) for arg in args])
284287

285288
if args and not formatted_args:
286289
try:
287290
iter(args)
288291
except TypeError:
289-
fmt = [format_annotation(args, config)]
292+
fmt = [format_annotation(args, config, short_literals=short_literals)]
290293
else:
291-
fmt = [format_annotation(arg, config) for arg in args]
294+
fmt = [format_annotation(arg, config, short_literals=short_literals) for arg in args]
292295
formatted_args = args_format.format(", ".join(fmt))
293296

294297
escape = "\\ " if formatted_args else ""
@@ -783,7 +786,10 @@ def _inject_signature(
783786
if annotation is None:
784787
type_annotation = f":type {arg_name}: "
785788
else:
786-
formatted_annotation = add_type_css_class(format_annotation(annotation, app.config))
789+
short_literals = app.config.python_display_short_literal_types
790+
formatted_annotation = add_type_css_class(
791+
format_annotation(annotation, app.config, short_literals=short_literals)
792+
)
787793
type_annotation = f":type {arg_name}: {formatted_annotation}"
788794

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

926-
formatted_annotation = add_type_css_class(format_annotation(type_hints["return"], app.config))
932+
short_literals = app.config.python_display_short_literal_types
933+
formatted_annotation = add_type_css_class(
934+
format_annotation(type_hints["return"], app.config, short_literals=short_literals)
935+
)
927936

928937
if r.found_param and insert_index < len(lines) and lines[insert_index].strip():
929938
insert_index -= 1

src/sphinx_autodoc_typehints/attributes_patch.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@
4242
orig_handle_signature = PyAttribute.handle_signature
4343

4444

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

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

5252

5353
def patch_attribute_documenter(app: Sphinx) -> None:

tests/conftest.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import re
44
import shutil
55
import sys
6+
from contextlib import suppress
67
from pathlib import Path
78
from typing import TYPE_CHECKING
89

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

4544

4645
@pytest.fixture

tests/test_integration.py

+55-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import ( # no type comments
1010
TYPE_CHECKING,
1111
Any,
12+
Literal,
1213
NewType,
1314
Optional,
1415
TypeVar,
@@ -661,6 +662,59 @@ def func_with_overload(a: Union[int, str], b: Union[int, str]) -> None:
661662
"""
662663

663664

665+
@expected(
666+
"""\
667+
mod.func_literals_long_format(a, b)
668+
669+
A docstring.
670+
671+
Parameters:
672+
* **a** ("Literal"["'arg1'", "'arg2'"]) -- Argument that can
673+
take either of two literal values.
674+
675+
* **b** ("Literal"["'arg1'", "'arg2'"]) -- Argument that can
676+
take either of two literal values.
677+
678+
Return type:
679+
"None"
680+
""",
681+
)
682+
def func_literals_long_format(a: Literal["arg1", "arg2"], b: Literal["arg1", "arg2"]) -> None:
683+
"""
684+
A docstring.
685+
686+
:param a: Argument that can take either of two literal values.
687+
:param b: Argument that can take either of two literal values.
688+
"""
689+
690+
691+
@expected(
692+
"""\
693+
mod.func_literals_short_format(a, b)
694+
695+
A docstring.
696+
697+
Parameters:
698+
* **a** ("'arg1'" | "'arg2'") -- Argument that can take either
699+
of two literal values.
700+
701+
* **b** ("'arg1'" | "'arg2'") -- Argument that can take either
702+
of two literal values.
703+
704+
Return type:
705+
"None"
706+
""",
707+
python_display_short_literal_types=True,
708+
)
709+
def func_literals_short_format(a: Literal["arg1", "arg2"], b: Literal["arg1", "arg2"]) -> None:
710+
"""
711+
A docstring.
712+
713+
:param a: Argument that can take either of two literal values.
714+
:param b: Argument that can take either of two literal values.
715+
"""
716+
717+
664718
@expected(
665719
"""\
666720
class mod.TestClassAttributeDocs
@@ -1386,7 +1440,7 @@ def has_doctest1() -> None:
13861440
Unformatted = TypeVar("Unformatted")
13871441

13881442

1389-
@warns("cannot cache unpickable configuration value: 'typehints_formatter'")
1443+
@warns("cannot cache unpickleable configuration value: 'typehints_formatter'")
13901444
@expected(
13911445
"""
13921446
mod.typehints_formatter_applied_to_signature(param: Formatted) -> Formatted

tests/test_sphinx_autodoc_typehints.py

+1-12
Original file line numberDiff line numberDiff line change
@@ -554,17 +554,6 @@ class dummy_module.DataClass(x)
554554
assert contents == expected_contents
555555

556556

557-
def maybe_fix_py310(expected_contents: str) -> str:
558-
if sys.version_info >= (3, 11):
559-
return expected_contents
560-
561-
for old, new in [
562-
('"str" | "None"', '"Optional"["str"]'),
563-
]:
564-
expected_contents = expected_contents.replace(old, new)
565-
return expected_contents
566-
567-
568557
@pytest.mark.sphinx("text", testroot="dummy")
569558
@patch("sphinx.writers.text.MAXWIDTH", 2000)
570559
def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) -> None:
@@ -595,7 +584,7 @@ def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO)
595584
"str"
596585
"""
597586
expected_contents = dedent(expected_contents)
598-
expected_contents = maybe_fix_py310(dedent(expected_contents))
587+
expected_contents = dedent(expected_contents)
599588
assert contents == expected_contents
600589

601590

tox.ini

+5-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
[tox]
22
requires =
3-
tox>=4.23.2
4-
tox-uv>=1.16.2
3+
tox>=4.24.1
4+
tox-uv>=1.24
55
env_list =
66
fix
77
3.13
88
3.12
99
3.11
10-
3.10
1110
type
1211
pkg_meta
1312
skip_missing_interpreters = true
@@ -45,7 +44,7 @@ commands =
4544
[testenv:type]
4645
description = run type check on code base
4746
deps =
48-
mypy==1.14
47+
mypy==1.15
4948
types-docutils>=0.21.0.20241128
5049
commands =
5150
mypy src
@@ -56,8 +55,8 @@ description = check that the long description is valid
5655
skip_install = true
5756
deps =
5857
check-wheel-contents>=0.6.1
59-
twine>=6.0.1
60-
uv>=0.5.11
58+
twine>=6.1
59+
uv>=0.6.1
6160
commands =
6261
uv build --sdist --wheel --out-dir {env_tmp_dir} .
6362
twine check {env_tmp_dir}{/}*

0 commit comments

Comments
 (0)