Skip to content

Commit 6c906da

Browse files
⬆️ Add Sphinx 9 support (#255)
This PR adds support for Sphinx 9.x while maintaining backward compatibility with Sphinx 7.x and 8.x. The changes address a breaking change in docutils 0.22+ regarding boolean attribute serialization in XML output. **Changes:** - Updated dependency version constraints to support Sphinx 9 and myst-parser 4-5 - Added Sphinx 9.0 to the CI test matrix for Ubuntu and Windows environments - Introduced XML normalization fixture to handle docutils 0.22+ boolean serialization changes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 6325b6f commit 6c906da

File tree

4 files changed

+69
-15
lines changed

4 files changed

+69
-15
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,21 @@ jobs:
2828
matrix:
2929
os: [ubuntu-latest]
3030
python-version: ["3.11", "3.12", "3.13", "3.14"]
31-
sphinx-version: ["~=7.0", "~=8.0"]
31+
sphinx-version: ["~=7.0", "~=8.0", "~=9.0"]
3232
extras: ["testing"]
3333
include:
3434
- os: windows-latest
3535
python-version: "3.11"
3636
sphinx-version: "~=7.0"
3737
extras: "testing"
3838
- os: windows-latest
39-
python-version: "3.14"
39+
python-version: "3.13"
4040
sphinx-version: "~=8.0"
4141
extras: "testing"
42+
- os: windows-latest
43+
python-version: "3.14"
44+
sphinx-version: "~=9.0"
45+
extras: "testing"
4246

4347
runs-on: ${{ matrix.os }}
4448

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,17 @@ classifiers = [
2727
]
2828
keywords = ["sphinx", "extension", "material design", "web components"]
2929
requires-python = ">=3.11"
30-
dependencies = ["sphinx>=7,<9"]
30+
dependencies = ["sphinx>=7,<10"]
3131

3232
[project.urls]
3333
Homepage = "https://github.com/executablebooks/sphinx-design"
3434
Documentation = "https://sphinx-design.readthedocs.io"
3535

3636
[project.optional-dependencies]
3737
code-style = ["pre-commit>=3,<4"]
38-
rtd = ["myst-parser>=3,<5"]
38+
rtd = ["myst-parser>=4,<6"]
3939
testing = [
40-
"myst-parser>=3,<5",
40+
"myst-parser>=4,<6",
4141
"pytest~=8.3",
4242
"pytest-cov",
4343
"pytest-regressions",

tests/conftest.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import os
22
from pathlib import Path
3+
import re
34
from typing import Any
45

6+
from docutils import __version_info__ as docutils_version_info
57
from docutils import nodes
68
import pytest
79
from sphinx import version_info
@@ -88,3 +90,39 @@ def _create_project(
8890
return SphinxBuilder(app, src_path)
8991

9092
yield _create_project
93+
94+
95+
DOCUTILS_0_22_PLUS = docutils_version_info >= (0, 22)
96+
97+
98+
@pytest.fixture
99+
def normalize_doctree_xml():
100+
"""Normalize docutils XML output for cross-version compatibility.
101+
102+
In docutils 0.22+, boolean attributes are serialized as "1"/"0"
103+
instead of "True"/"False". This function normalizes to the old format
104+
for consistent test fixtures.
105+
"""
106+
107+
def _normalize(text: str) -> str:
108+
if DOCUTILS_0_22_PLUS:
109+
# Normalize new format (1/0) to old format (1/0)
110+
# Only replace when it's clearly a boolean attribute value
111+
# Pattern: attribute="1" or attribute="0"
112+
attrs = [
113+
"checked",
114+
"force",
115+
"has_title",
116+
"internal",
117+
"is_div",
118+
"linenos",
119+
"opened",
120+
"refexplicit",
121+
"refwarn",
122+
"selected",
123+
]
124+
text = re.sub(rf' ({"|".join(attrs)})="1"', r' \1="True"', text)
125+
text = re.sub(rf' ({"|".join(attrs)})="0"', r' \1="False"', text)
126+
return text
127+
128+
return _normalize

tests/test_snippets.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ def write_assets(src_path: Path):
3333
ids=[path.name[: -len(path.suffix)] for path in SNIPPETS_GLOB_RST],
3434
)
3535
def test_snippets_rst(
36-
sphinx_builder: Callable[..., SphinxBuilder], path: Path, file_regression
36+
sphinx_builder: Callable[..., SphinxBuilder],
37+
path: Path,
38+
file_regression,
39+
normalize_doctree_xml,
3740
):
3841
"""Test snippets written in RestructuredText (before post-transforms)."""
3942
builder = sphinx_builder(conf_kwargs={"extensions": ["sphinx_design"]})
@@ -44,7 +47,7 @@ def test_snippets_rst(
4447
doctree = builder.get_doctree("index", post_transforms=False)
4548
doctree.attributes.pop("translation_progress", None) # added in sphinx 7.1
4649
file_regression.check(
47-
doctree.pformat(),
50+
normalize_doctree_xml(doctree.pformat()),
4851
basename=f"snippet_pre_{path.name[: -len(path.suffix)]}",
4952
extension=".xml",
5053
encoding="utf8",
@@ -58,7 +61,10 @@ def test_snippets_rst(
5861
)
5962
@pytest.mark.skipif(not MYST_INSTALLED, reason="myst-parser not installed")
6063
def test_snippets_myst(
61-
sphinx_builder: Callable[..., SphinxBuilder], path: Path, file_regression
64+
sphinx_builder: Callable[..., SphinxBuilder],
65+
path: Path,
66+
file_regression,
67+
normalize_doctree_xml,
6268
):
6369
"""Test snippets written in MyST Markdown (before post-transforms)."""
6470
builder = sphinx_builder()
@@ -69,7 +75,7 @@ def test_snippets_myst(
6975
doctree = builder.get_doctree("index", post_transforms=False)
7076
doctree.attributes.pop("translation_progress", None) # added in sphinx 7.1
7177
file_regression.check(
72-
doctree.pformat(),
78+
normalize_doctree_xml(doctree.pformat()),
7379
basename=f"snippet_pre_{path.name[: -len(path.suffix)]}",
7480
extension=".xml",
7581
encoding="utf8",
@@ -82,7 +88,10 @@ def test_snippets_myst(
8288
ids=[path.name[: -len(path.suffix)] for path in SNIPPETS_GLOB_RST],
8389
)
8490
def test_snippets_rst_post(
85-
sphinx_builder: Callable[..., SphinxBuilder], path: Path, file_regression
91+
sphinx_builder: Callable[..., SphinxBuilder],
92+
path: Path,
93+
file_regression,
94+
normalize_doctree_xml,
8695
):
8796
"""Test snippets written in RestructuredText (after HTML post-transforms)."""
8897
builder = sphinx_builder(conf_kwargs={"extensions": ["sphinx_design"]})
@@ -93,7 +102,7 @@ def test_snippets_rst_post(
93102
doctree = builder.get_doctree("index", post_transforms=True)
94103
doctree.attributes.pop("translation_progress", None) # added in sphinx 7.1
95104
file_regression.check(
96-
doctree.pformat(),
105+
normalize_doctree_xml(doctree.pformat()),
97106
basename=f"snippet_post_{path.name[: -len(path.suffix)]}",
98107
extension=".xml",
99108
encoding="utf8",
@@ -107,7 +116,10 @@ def test_snippets_rst_post(
107116
)
108117
@pytest.mark.skipif(not MYST_INSTALLED, reason="myst-parser not installed")
109118
def test_snippets_myst_post(
110-
sphinx_builder: Callable[..., SphinxBuilder], path: Path, file_regression
119+
sphinx_builder: Callable[..., SphinxBuilder],
120+
path: Path,
121+
file_regression,
122+
normalize_doctree_xml,
111123
):
112124
"""Test snippets written in MyST Markdown (after HTML post-transforms)."""
113125
builder = sphinx_builder()
@@ -118,7 +130,7 @@ def test_snippets_myst_post(
118130
doctree = builder.get_doctree("index", post_transforms=True)
119131
doctree.attributes.pop("translation_progress", None) # added in sphinx 7.1
120132
file_regression.check(
121-
doctree.pformat(),
133+
normalize_doctree_xml(doctree.pformat()),
122134
basename=f"snippet_post_{path.name[: -len(path.suffix)]}",
123135
extension=".xml",
124136
encoding="utf8",
@@ -164,7 +176,7 @@ def test_sd_hide_title_myst(
164176

165177
@pytest.mark.skipif(not MYST_INSTALLED, reason="myst-parser not installed")
166178
def test_sd_custom_directives(
167-
sphinx_builder: Callable[..., SphinxBuilder], file_regression
179+
sphinx_builder: Callable[..., SphinxBuilder], file_regression, normalize_doctree_xml
168180
):
169181
"""Test that the defaults are used."""
170182
builder = sphinx_builder(
@@ -188,7 +200,7 @@ def test_sd_custom_directives(
188200
doctree = builder.get_doctree("index", post_transforms=False)
189201
doctree.attributes.pop("translation_progress", None) # added in sphinx 7.1
190202
file_regression.check(
191-
doctree.pformat(),
203+
normalize_doctree_xml(doctree.pformat()),
192204
basename="sd_custom_directives",
193205
extension=".xml",
194206
encoding="utf8",

0 commit comments

Comments
 (0)