Skip to content

Commit 19512c0

Browse files
authored
⬆️ Allow docutils 0.22 (#1084)
This PR updates the project to support docutils 0.22 by adapting to breaking changes in how docutils serializes boolean attributes and line numbers. The changes ensure test compatibility across docutils versions 0.20-0.22. **Changes:** - Updated docutils version constraint from `<0.22` to `<0.23` - Introduced `normalize_doctree_xml()` function to handle cross-version XML serialization differences - Updated test fixtures to reflect docutils 0.22's line numbering changes Note, one issue was found and raised upstream: sphinx-doc/sphinx#14261
1 parent a9e529f commit 19512c0

19 files changed

Lines changed: 169 additions & 40 deletions

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ jobs:
7474
strategy:
7575
fail-fast: false
7676
matrix:
77-
docutils-version: ["0.20", "0.21"]
77+
docutils-version: ["0.20", "0.21", "0.22"]
7878

7979
steps:
8080
- name: Checkout source

AGENTS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ tox -e py311-sphinx8
8585
# Run with coverage
8686
tox -- --cov=myst_parser
8787

88-
# Update regression test fixtures
88+
# Update regression test fixtures (this will initially produce an error code if the files change)
89+
# but note, these files must pass for all python/sphinx/docutils versions
8990
tox -- --regen-file-failure --force-regen
9091
```
9192

@@ -337,5 +338,6 @@ flowchart TB
337338
- [markdown-it-py Documentation](https://markdown-it-py.readthedocs.io/)
338339
- [Docutils Repository](https://github.com/live-clones/docutils)
339340
- [Docutils Documentation](https://docutils.sourceforge.io/)
341+
- [Docutils release log](https://docutils.sourceforge.io/RELEASE-NOTES.html)
340342
- [Sphinx Repository](https://github.com/sphinx-doc/sphinx)
341343
- [Sphinx Extension Development](https://www.sphinx-doc.org/en/master/extdev/index.html)

myst_parser/mocking.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -454,12 +454,18 @@ def run(self) -> list[nodes.Element]:
454454
literal_block.line = 1 # TODO don;t think this should be 1?
455455
self.add_name(literal_block)
456456
if "number-lines" in self.options:
457-
try:
458-
startline = int(self.options["number-lines"] or 1)
459-
except ValueError as err:
460-
raise DirectiveError(
461-
3, ":number-lines: with non-integer start value"
462-
) from err
457+
# note starting in docutils 0.22 this option is now an integer instead of a string, see: https://github.com/live-clones/docutils/commit/f39ac1413e56a330c8fea6e0d080fed0ff2b8483
458+
if self.options["number-lines"] is None:
459+
startline = 1
460+
elif isinstance(self.options["number-lines"], int):
461+
startline = self.options["number-lines"]
462+
else:
463+
try:
464+
startline = int(self.options["number-lines"] or 1)
465+
except ValueError as err:
466+
raise DirectiveError(
467+
3, ":number-lines: with non-integer start value"
468+
) from err
463469
endline = startline + len(file_content.splitlines())
464470
file_content = file_content.removesuffix("\n")
465471
tokens = NumberLines([([], file_content)], startline, endline)

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ keywords = [
3434
]
3535
requires-python = ">=3.11"
3636
dependencies = [
37-
"docutils>=0.20,<0.22",
37+
"docutils>=0.20,<0.23",
3838
"jinja2", # required for substitutions, but let sphinx choose version
3939
"markdown-it-py~=4.0",
4040
"mdit-py-plugins~=0.5",
@@ -146,6 +146,7 @@ disallow_any_generics = false
146146
[tool.pytest.ini_options]
147147
filterwarnings = [
148148
"ignore:.*The default for the setting.*:FutureWarning",
149+
"ignore:.*:PendingDeprecationWarning",
149150
]
150151

151152
[tool.coverage.run]

tests/conftest.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Shared pytest configuration for all tests."""
2+
3+
import re
4+
5+
from docutils import __version_info__ as docutils_version_info
6+
7+
DOCUTILS_0_22_PLUS = docutils_version_info >= (0, 22)
8+
9+
10+
def normalize_doctree_xml(text: str) -> str:
11+
"""Normalize docutils XML output for cross-version compatibility.
12+
13+
In docutils 0.22+, boolean attributes are serialized as "1"/"0"
14+
instead of "True"/"False". This function normalizes to the old format
15+
for consistent test fixtures.
16+
"""
17+
if DOCUTILS_0_22_PLUS:
18+
# Normalize new format (1/0) to old format (1/0)
19+
# Only replace when it's clearly a boolean attribute value
20+
# Pattern: attribute="1" or attribute="0"
21+
attrs = [
22+
"force",
23+
"glob",
24+
"hidden",
25+
"id_link",
26+
"includehidden",
27+
"inline",
28+
"internal",
29+
"is_div",
30+
"linenos",
31+
"multi_line_parameter_list",
32+
"multi_line_trailing_comma",
33+
"no-contents-entry",
34+
"no-index",
35+
"no-index-entry",
36+
"no-typesetting",
37+
"no-wrap",
38+
"nocontentsentry",
39+
"noindex",
40+
"noindexentry",
41+
"nowrap",
42+
"refexplicit",
43+
"refspecific",
44+
"refwarn",
45+
"sorted",
46+
"titlesonly",
47+
"toctree",
48+
"translatable",
49+
]
50+
text = re.sub(rf' ({"|".join(attrs)})="1"', r' \1="True"', text)
51+
text = re.sub(rf' ({"|".join(attrs)})="0"', r' \1="False"', text)
52+
# numbered is changed in math_block, but not in toctree, so we have to be more precise
53+
text = re.sub(r' numbered="1" xml:space', r' numbered="True" xml:space', text)
54+
return text

tests/test_html/test_html_to_nodes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from myst_parser.config.main import MdParserConfig
88
from myst_parser.mdit_to_docutils.html_to_nodes import html_to_nodes
9+
from tests.conftest import normalize_doctree_xml
910

1011
FIXTURE_PATH = Path(__file__).parent
1112

@@ -32,4 +33,4 @@ def _run_directive(name: str, first_line: str, content: str, position: int):
3233
def test_html_to_nodes(file_params, mock_renderer):
3334
output = nodes.container()
3435
output += html_to_nodes(file_params.content, line_number=0, renderer=mock_renderer)
35-
file_params.assert_expected(output.pformat(), rstrip=True)
36+
file_params.assert_expected(normalize_doctree_xml(output.pformat()), rstrip=True)

tests/test_renderers/fixtures/docutil_link_resolution.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,15 @@
128128
Test
129129
<subtitle ids="other test-1" names="other test">
130130
Other
131-
<system_message backrefs="test-1" level="1" line="3" source="<src>/index.md" type="INFO">
132-
<paragraph>
133-
Duplicate implicit target name: "test".
134131
<target refid="test-1">
135132
<paragraph>
136133
<reference id_link="True" refid="test-1">
137134
<inline classes="std std-ref">
138135
Other
136+
137+
138+
<src>/index.md:3: (INFO/1) Target name overrides implicit target name "test".
139+
<src>/index.md:3: (INFO/1) Hyperlink target "test-1" is not referenced.
139140
.
140141

141142
[id-with-spaces]

tests/test_renderers/fixtures/myst-config.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,12 @@ a
263263
<document source="<string>">
264264
<paragraph>
265265
a
266+
<section classes="system-messages">
267+
<title>
268+
Docutils System Messages
269+
<system_message level="2" source="<string>" type="WARNING">
270+
<paragraph>
271+
The `attrs_image` extension is deprecated, please use `attrs_inline` instead. [myst.deprecated]
266272

267273
<string>:: (WARNING/2) The `attrs_image` extension is deprecated, please use `attrs_inline` instead. [myst.deprecated]
268274
.

tests/test_renderers/test_error_reporting.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,9 @@ def test_basic(file_params):
2020
parser=Parser(),
2121
settings_overrides={"warning_stream": report_stream},
2222
)
23-
file_params.assert_expected(report_stream.getvalue(), rstrip=True)
23+
text = report_stream.getvalue()
24+
# changed in docutils 0.23
25+
text = text.replace(
26+
"corresponding footnote available", "corresponding footnotes available"
27+
)
28+
file_params.assert_expected(text, rstrip=True)

tests/test_renderers/test_fixtures_docutils.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from typing import Any
1212

1313
import pytest
14+
from conftest import normalize_doctree_xml
15+
from docutils import __version_info__ as docutils_version
1416
from docutils.core import Publisher, publish_doctree
1517
from pytest_param_files import ParamTestData
1618

@@ -37,7 +39,9 @@ def _apply_transforms(self):
3739
)
3840

3941
# in docutils 0.18 footnote ids have changed
40-
outcome = doctree.pformat().replace('"footnote-reference-1"', '"id1"')
42+
outcome = normalize_doctree_xml(doctree.pformat()).replace(
43+
'"footnote-reference-1"', '"id1"'
44+
)
4145
outcome = outcome.replace(' language=""', "")
4246
file_params.assert_expected(outcome, rstrip_lines=True)
4347

@@ -48,13 +52,18 @@ def test_link_resolution(file_params: ParamTestData):
4852
settings = settings_from_cmdline(file_params.description)
4953
report_stream = StringIO()
5054
settings["warning_stream"] = report_stream
55+
if file_params.title == "explicit>implicit":
56+
if docutils_version < (0, 22):
57+
# reporting changed in docutils 0.22
58+
pytest.skip("different in docutils>=0.22")
59+
settings["report_level"] = 0
5160
doctree = publish_doctree(
5261
file_params.content,
5362
source_path="<src>/index.md",
5463
parser=Parser(),
5564
settings_overrides=settings,
5665
)
57-
outcome = doctree.pformat()
66+
outcome = normalize_doctree_xml(doctree.pformat())
5867
if report_stream.getvalue().strip():
5968
outcome += "\n\n" + report_stream.getvalue().strip()
6069
file_params.assert_expected(outcome, rstrip_lines=True)
@@ -75,7 +84,9 @@ def _apply_transforms(self):
7584
parser=Parser(),
7685
)
7786

78-
file_params.assert_expected(doctree.pformat(), rstrip_lines=True)
87+
file_params.assert_expected(
88+
normalize_doctree_xml(doctree.pformat()), rstrip_lines=True
89+
)
7990

8091

8192
@pytest.mark.param_file(FIXTURE_PATH / "docutil_directives.md")
@@ -95,7 +106,9 @@ def _apply_transforms(self):
95106
parser=Parser(),
96107
)
97108

98-
file_params.assert_expected(doctree.pformat(), rstrip_lines=True)
109+
file_params.assert_expected(
110+
normalize_doctree_xml(doctree.pformat()), rstrip_lines=True
111+
)
99112

100113

101114
@pytest.mark.param_file(FIXTURE_PATH / "docutil_syntax_extensions.txt")
@@ -109,7 +122,9 @@ def test_syntax_extensions(file_params: ParamTestData):
109122
parser=Parser(),
110123
settings_overrides=settings,
111124
)
112-
file_params.assert_expected(doctree.pformat(), rstrip_lines=True)
125+
file_params.assert_expected(
126+
normalize_doctree_xml(doctree.pformat()), rstrip_lines=True
127+
)
113128

114129

115130
def settings_from_cmdline(cmdline: str | None) -> dict[str, Any]:

0 commit comments

Comments
 (0)