Skip to content

Commit 16857cd

Browse files
Fix tests for Sphinx 8.1, remove Python 3.9 support (#178)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Philipp A. <[email protected]>
1 parent a94d488 commit 16857cd

14 files changed

+116
-85
lines changed

.github/workflows/ci.yml

+5-5
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
runs-on: ubuntu-latest
2323
strategy:
2424
matrix:
25-
python-version: ["3.9", "3.10", "3.11", "3.12"]
25+
python-version: ["3.10", "3.11", "3.12"]
2626
steps:
2727
- uses: actions/checkout@v4
2828
with:
@@ -31,11 +31,11 @@ jobs:
3131
- uses: actions/setup-python@v5
3232
with:
3333
python-version: ${{ matrix.python-version }}
34-
cache: pip
34+
- uses: hynek/setup-cached-uv@v2
35+
with:
36+
cache-dependency-path: pyproject.toml
3537
- name: dependencies
36-
run: |
37-
pip install --upgrade pip wheel
38-
pip install .[test,typehints,myst] coverage-rich 'anyconfig[toml] >=0.14'
38+
run: uv pip install --system .[test,typehints,myst] coverage-rich 'anyconfig[toml] >=0.14'
3939
- name: tests
4040
run: coverage run -m pytest --verbose --color=yes
4141
- name: show coverage

.pre-commit-config.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ repos:
44
hooks:
55
- id: trailing-whitespace
66
- repo: https://github.com/astral-sh/ruff-pre-commit
7-
rev: v0.6.9
7+
rev: v0.7.0
88
hooks:
99
- id: ruff
1010
args: [--fix, --exit-non-zero-on-fix]
@@ -17,7 +17,7 @@ repos:
1717
1818
1919
- repo: https://github.com/pre-commit/mirrors-mypy
20-
rev: v1.11.2
20+
rev: v1.12.1
2121
hooks:
2222
- id: mypy
2323
additional_dependencies:

.readthedocs.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
version: 2
22
build:
3-
os: ubuntu-22.04
3+
os: ubuntu-24.04
44
tools:
5-
python: "3.11"
5+
python: "3.12"
66
python:
77
install:
88
- method: pip

pyproject.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@ classifiers = [
1717
'Framework :: Sphinx :: Extension',
1818
'Typing :: Typed',
1919
]
20-
requires-python = '>=3.9'
20+
requires-python = '>=3.10'
2121
dependencies = [
2222
'sphinx>=7.0',
23-
'get-annotations; python_version < "3.10"',
2423
]
2524

2625
[project.optional-dependencies]
@@ -30,6 +29,7 @@ test = [
3029
'coverage',
3130
'legacy-api-wrap',
3231
'defusedxml', # sphinx[test] would also pull in cython
32+
'sphinx>=8.1.0' # https://github.com/sphinx-doc/sphinx/pull/12743
3333
]
3434
doc = [
3535
'scanpydoc[typehints,myst,theme]',

src/scanpydoc/elegant_typehints/_formatting.py

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import sys
43
import inspect
54
from types import GenericAlias
65
from typing import TYPE_CHECKING, Any, cast, get_args, get_origin
@@ -15,12 +14,6 @@
1514
from sphinx.config import Config
1615

1716

18-
if sys.version_info >= (3, 10):
19-
from types import UnionType
20-
else: # pragma: no cover
21-
UnionType = None
22-
23-
2417
def typehints_formatter(annotation: type[Any], config: Config) -> str | None:
2518
"""Generate reStructuredText containing links to the types.
2619
@@ -43,7 +36,7 @@ def typehints_formatter(annotation: type[Any], config: Config) -> str | None:
4336

4437
tilde = "" if config.typehints_fully_qualified else "~"
4538

46-
if isinstance(annotation, (GenericAlias, _GenericAlias)):
39+
if isinstance(annotation, GenericAlias | _GenericAlias):
4740
args = get_args(annotation)
4841
annotation = cast(type[Any], get_origin(annotation))
4942
else:

src/scanpydoc/elegant_typehints/_return_tuple.py

+3-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from __future__ import annotations
22

33
import re
4-
import sys
54
import inspect
5+
from types import UnionType
66
from typing import TYPE_CHECKING, Union, get_args, get_origin, get_type_hints
77
from typing import Tuple as t_Tuple # noqa: UP035
88
from logging import getLogger
@@ -19,12 +19,7 @@
1919
from sphinx.ext.autodoc import Options
2020

2121

22-
if sys.version_info > (3, 10):
23-
from types import UnionType
24-
25-
UNION_TYPES = {Union, UnionType}
26-
else: # pragma: no cover
27-
UNION_TYPES = {Union}
22+
UNION_TYPES = {Union, UnionType}
2823

2924

3025
__all__ = ["process_docstring", "_parse_returns_section", "setup"]
@@ -77,7 +72,7 @@ def process_docstring( # noqa: PLR0913
7772

7873
idxs_ret_names = _get_idxs_ret_names(lines)
7974
if len(idxs_ret_names) == len(ret_types):
80-
for l, rt in zip(idxs_ret_names, ret_types):
75+
for l, rt in zip(idxs_ret_names, ret_types, strict=False):
8176
typ = format_annotation(rt, app.config)
8277
if (line := lines[l]).lstrip() in {":returns: :", ":return: :", ":"}:
8378
transformed = f"{line[:-1]}{typ}"

src/scanpydoc/rtd_github_links/__init__.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,7 @@ def _infer_vars(config: Config) -> tuple[str, PurePosixPath]:
118118

119119

120120
def _get_annotations(obj: _SourceObjectType) -> dict[str, Any]:
121-
if sys.version_info > (3, 10):
122-
from inspect import get_annotations
123-
else: # pragma: no cover
124-
from get_annotations import get_annotations
121+
from inspect import get_annotations
125122

126123
try:
127124
return get_annotations(obj) # type: ignore[no-any-return,arg-type,unused-ignore]
@@ -159,7 +156,7 @@ def _get_obj_module(qualname: str) -> tuple[Any, ModuleType]:
159156
raise e from None
160157
if isinstance(thing, ModuleType): # pragma: no cover
161158
mod = thing
162-
elif is_dataclass(obj) or isinstance(thing, (GenericAlias, _GenericAlias)):
159+
elif is_dataclass(obj) or isinstance(thing, GenericAlias | _GenericAlias):
163160
obj = thing
164161
else:
165162
obj = thing

src/scanpydoc/testing.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Testing utilities."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING, Protocol
6+
7+
8+
if TYPE_CHECKING:
9+
from typing import Any
10+
11+
from sphinx.testing.util import SphinxTestApp
12+
13+
14+
class MakeApp(Protocol):
15+
"""Create a SphinxTestApp instance."""
16+
17+
def __call__( # noqa: D102
18+
self,
19+
builder: str = "html",
20+
/,
21+
*,
22+
exception_on_warning: bool = False,
23+
**conf: Any, # noqa: ANN401
24+
) -> SphinxTestApp: ...

tests/conftest.py

+19-6
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,29 @@
1818
from pathlib import Path
1919
from collections.abc import Callable, Generator
2020

21-
from sphinx.application import Sphinx
21+
from sphinx.testing.util import SphinxTestApp
22+
23+
from scanpydoc.testing import MakeApp
2224

2325

2426
@pytest.fixture
25-
def make_app_setup(
26-
make_app: Callable[..., Sphinx], tmp_path: Path
27-
) -> Callable[..., Sphinx]:
28-
def make_app_setup(builder: str = "html", /, **conf: Any) -> Sphinx: # noqa: ANN401
27+
def make_app_setup(make_app: type[SphinxTestApp], tmp_path: Path) -> MakeApp:
28+
def make_app_setup(
29+
builder: str = "html",
30+
/,
31+
*,
32+
exception_on_warning: bool = False,
33+
**conf: Any, # noqa: ANN401
34+
) -> SphinxTestApp:
2935
(tmp_path / "conf.py").write_text("")
30-
return make_app(buildername=builder, srcdir=tmp_path, confoverrides=conf)
36+
conf.setdefault("suppress_warnings", []).append("app.add_node")
37+
return make_app(
38+
buildername=builder,
39+
srcdir=tmp_path,
40+
confoverrides=conf,
41+
warningiserror=exception_on_warning,
42+
exception_on_warning=exception_on_warning,
43+
)
3144

3245
return make_app_setup
3346

tests/test_base.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@
1111

1212

1313
if TYPE_CHECKING:
14-
from collections.abc import Callable
15-
1614
import pytest
1715
from sphinx.application import Sphinx
1816

17+
from scanpydoc.testing import MakeApp
18+
1919

2020
DEPRECATED = frozenset({"scanpydoc.autosummary_generate_imported"})
2121

2222

2323
def test_all_get_installed(
24-
monkeypatch: pytest.MonkeyPatch, make_app_setup: Callable[..., Sphinx]
24+
monkeypatch: pytest.MonkeyPatch, make_app_setup: MakeApp
2525
) -> None:
2626
setups_seen: set[str] = set()
2727
setups_called: dict[str, Sphinx] = {}

tests/test_definition_list_typed_field.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111

1212

1313
if TYPE_CHECKING:
14-
from collections.abc import Callable
15-
1614
from sphinx.application import Sphinx
1715

16+
from scanpydoc.testing import MakeApp
17+
1818

1919
@pytest.fixture
20-
def app(make_app_setup: Callable[..., Sphinx]) -> Sphinx:
20+
def app(make_app_setup: MakeApp) -> Sphinx:
2121
app = make_app_setup()
2222
app.setup_extension("scanpydoc.definition_list_typed_field")
2323
return app
@@ -41,7 +41,7 @@ def app(make_app_setup: Callable[..., Sphinx]) -> Sphinx:
4141
"""
4242

4343

44-
def test_apps_separate(app: Sphinx, make_app_setup: Callable[..., Sphinx]) -> None:
44+
def test_apps_separate(app: Sphinx, make_app_setup: MakeApp) -> None:
4545
app_no_setup = make_app_setup()
4646
assert app is not app_no_setup
4747
assert "scanpydoc.definition_list_typed_field" in app.extensions
@@ -97,11 +97,12 @@ def test_convert_params(
9797
assert isinstance(cyr := term[2], nodes.classifier)
9898
assert len(cyr) == len(conv_types), cyr.children
9999
assert all(
100-
isinstance(cyr_part, conv_type) for cyr_part, conv_type in zip(cyr, conv_types)
100+
isinstance(cyr_part, conv_type)
101+
for cyr_part, conv_type in zip(cyr, conv_types, strict=True)
101102
)
102103

103104

104-
def test_load_error(make_app_setup: Callable[..., Sphinx]) -> None:
105+
def test_load_error(make_app_setup: MakeApp) -> None:
105106
with pytest.raises(RuntimeError, match=r"Please load sphinx\.ext\.napoleon before"):
106107
make_app_setup(
107108
extensions=["scanpydoc.definition_list_typed_field", "sphinx.ext.napoleon"]

tests/test_elegant_typehints.py

+10-19
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,9 @@
33
from __future__ import annotations
44

55
import re
6-
import sys
76
import inspect
87
from io import StringIO
9-
from typing import (
10-
TYPE_CHECKING,
11-
Any,
12-
Union,
13-
AnyStr,
14-
NoReturn,
15-
Optional,
16-
cast,
17-
get_origin,
18-
)
8+
from typing import TYPE_CHECKING, Any, AnyStr, NoReturn, cast, get_origin
199
from pathlib import Path
2010
from operator import attrgetter
2111
from collections.abc import Mapping, Callable
@@ -31,6 +21,8 @@
3121

3222
from sphinx.application import Sphinx
3323

24+
from scanpydoc.testing import MakeApp
25+
3426
class ProcessDoc(Protocol): # noqa: D101
3527
def __call__( # noqa: D102
3628
self, fn: Callable[..., Any], *, run_napoleon: bool = False
@@ -60,7 +52,7 @@ class Gen(Generic[T]): pass
6052

6153

6254
@pytest.fixture
63-
def app(make_app_setup: Callable[..., Sphinx]) -> Sphinx:
55+
def app(make_app_setup: MakeApp) -> Sphinx:
6456
return make_app_setup(
6557
master_doc="index",
6658
extensions=[
@@ -256,8 +248,8 @@ def fn_test(m: object) -> None: # pragma: no cover
256248
AnyStr,
257249
NoReturn,
258250
Callable[[int], None],
259-
Union[int, str],
260-
Union[int, str, None],
251+
int | str,
252+
int | str | None,
261253
],
262254
ids=lambda p: str(p).replace("typing.", ""),
263255
)
@@ -274,7 +266,6 @@ def test_typing_classes(app: Sphinx, annotation: type) -> None:
274266
assert output is None or output.startswith(f":py:data:`typing.{name}")
275267

276268

277-
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires Python 3.10+")
278269
def test_union_type(app: Sphinx) -> None:
279270
union = eval("int | str") # noqa: S307
280271
assert typehints_formatter(union, app.config) is None
@@ -389,15 +380,15 @@ class B:
389380
("return_ann", "foo_rendered"),
390381
[
391382
pytest.param(tuple[str, int], ":py:class:`str`", id="tuple"),
392-
pytest.param(Optional[tuple[str, int]], ":py:class:`str`", id="tuple | None"),
383+
pytest.param(tuple[str, int] | None, ":py:class:`str`", id="tuple | None"),
393384
pytest.param(
394385
tuple[Mapping[str, float], int],
395386
r":py:class:`~collections.abc.Mapping`\ \["
396387
":py:class:`str`, :py:class:`float`"
397388
"]",
398389
id="complex",
399390
),
400-
pytest.param(Optional[int], None, id="int | None"),
391+
pytest.param(int | None, None, id="int | None"),
401392
],
402393
)
403394
def test_return_tuple(
@@ -472,14 +463,14 @@ def fn() -> tuple[int, str]: # pragma: no cover
472463
assert res[2].startswith(":rtype: :sphinx_autodoc_typehints_type:")
473464

474465

475-
def test_load_without_sat(make_app_setup: Callable[..., Sphinx]) -> None:
466+
def test_load_without_sat(make_app_setup: MakeApp) -> None:
476467
make_app_setup(
477468
master_doc="index",
478469
extensions=["sphinx.ext.autodoc", "scanpydoc.elegant_typehints"],
479470
)
480471

481472

482-
def test_load_error(make_app_setup: Callable[..., Sphinx]) -> None:
473+
def test_load_error(make_app_setup: MakeApp) -> None:
483474
with pytest.raises(
484475
RuntimeError,
485476
match=r"`scanpydoc.elegant_typehints` requires `sphinx.ext.autodoc`",

0 commit comments

Comments
 (0)