Skip to content

Commit 4f4096d

Browse files
choldgrafbsipocz
andauthored
Update infrastructure for new pythons and prep for latest sphinx (#706)
* Helper function for env app deprecation * More updates for latest sphinx * Add compatibility function for app * Update to latest python and sphinx versions * Clean up the env handler * More updates for new versions * remove 3.9 tests * Stop testing sphinx 9 * no variables * Adding some narrative to version limits and consistency between requirements and tox configs * Have to add the upper pin as we are still not compatible --------- Co-authored-by: Brigitta Sipőcz <bsipocz@gmail.com>
1 parent 5e06ea0 commit 4f4096d

21 files changed

Lines changed: 91 additions & 73 deletions

.github/workflows/tests.yml

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,26 @@ jobs:
1919
fail-fast: false
2020
matrix:
2121
os: [ubuntu-latest]
22-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
23-
sphinx: [""] # Newest Sphinx (any)
22+
python-version: ["3.10", "3.11", "3.12", "3.13"]
23+
sphinx: ["~=8.0"] # temporary limit, bring it back to newest sphinx once we are sphinx9 compatible
2424
myst-parser: [""] # Newest MyST Parser (any)
2525
include:
2626
# Just check the other platforms once
2727
- os: windows-latest
28-
python-version: "3.12"
28+
python-version: "3.13"
2929
sphinx: "~=8.0"
3030
myst-parser: "~=4.0"
3131
pillow: "==11.0.0"
3232
- os: macos-latest
33-
python-version: "3.12"
33+
python-version: "3.13"
3434
sphinx: "~=8.0"
3535
myst-parser: "~=4.0"
3636
pillow: "==11.0.0"
3737
- os: ubuntu-latest
38-
python-version: "3.12"
38+
python-version: "3.13"
3939
sphinx: "~=8.0"
4040
myst-parser: "~=4.0"
4141
pillow: "==11.0.0"
42-
# Oldest known-compatible dependencies
43-
- os: ubuntu-latest
44-
python-version: "3.9"
45-
sphinx: "==5.0.0"
46-
myst-parser: "==1.0.0"
47-
pillow: "==11.0.0"
4842
# Mid-range dependencies
4943
- os: ubuntu-latest
5044
python-version: "3.11"
@@ -53,8 +47,8 @@ jobs:
5347
pillow: "==11.0.0"
5448
# Newest known-compatible dependencies
5549
- os: ubuntu-latest
56-
python-version: "3.12"
57-
sphinx: "==8.0.2"
50+
python-version: "3.13"
51+
sphinx: "~=8.0"
5852
myst-parser: "==4.0.0"
5953
pillow: "==11.0.0"
6054

myst_nb/_compat.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1+
from typing import Any
2+
13
from docutils.nodes import Element
24

35

46
def findall(node: Element):
57
# findall replaces traverse in docutils v0.18
68
# note a difference is that findall is an iterator
79
return getattr(node, "findall", node.traverse)
10+
11+
12+
def get_env_app(env: Any):
13+
"""Return the Sphinx app without triggering deprecated accessors."""
14+
# This is the new Sphinx app behavior
15+
app = getattr(env, "_app", None)
16+
if app is None:
17+
# This was removed in Sphinx 9
18+
app = env.app
19+
return app

myst_nb/core/render.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from nbformat import NotebookNode
2828
from typing_extensions import Protocol
2929

30+
from myst_nb._compat import get_env_app
3031
from myst_nb.core.config import NbParserConfig
3132
from myst_nb.core.execute import NotebookClientBase
3233
from myst_nb.core.loggers import LoggerType # DEFAULT_LOG_TYPE,
@@ -405,7 +406,8 @@ def write_file(
405406
# Can't get relative path between drives on Windows
406407
return filepath.as_posix()
407408
# Path().relative_to() doesn't work when not a direct subpath
408-
return "/" + os.path.relpath(filepath, self.renderer.sphinx_env.app.srcdir)
409+
app = get_env_app(self.renderer.sphinx_env)
410+
return "/" + os.path.relpath(filepath, app.srcdir)
409411
else:
410412
return str(filepath)
411413

myst_nb/ext/download.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from sphinx.addnodes import download_reference
66
from sphinx.util.docutils import ReferenceRole
77

8+
from myst_nb._compat import get_env_app
89
from myst_nb.sphinx_ import SphinxEnvType
910

1011

@@ -21,7 +22,7 @@ def run(self):
2122
reftarget = (
2223
path.as_posix()
2324
if os.name == "nt"
24-
else ("/" + os.path.relpath(path, self.env.app.srcdir))
25+
else ("/" + os.path.relpath(path, get_env_app(self.env).srcdir))
2526
)
2627
node = download_reference(self.rawtext, reftarget=reftarget)
2728
self.set_source_info(node)

myst_nb/ext/glue/crossref.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from sphinx.transforms.post_transforms import SphinxPostTransform
2222
from sphinx.util import logging as sphinx_logging
2323

24-
from myst_nb._compat import findall
24+
from myst_nb._compat import findall, get_env_app
2525
from myst_nb.core.loggers import DEFAULT_LOG_TYPE
2626
from myst_nb.core.render import get_mime_priority
2727
from myst_nb.core.variables import format_plain_text
@@ -54,7 +54,8 @@ class ReplacePendingGlueReferences(SphinxPostTransform):
5454
def apply(self, **kwargs):
5555
"""Apply the transform."""
5656
cache_folder = self.env.mystnb_config.output_folder # type: ignore
57-
bname = self.app.builder.name
57+
app = get_env_app(self.env)
58+
bname = app.builder.name
5859
priority_list = get_mime_priority(
5960
bname, self.config["nb_mime_priority_overrides"]
6061
)
@@ -79,7 +80,7 @@ def apply(self, **kwargs):
7980
node,
8081
output,
8182
priority_list,
82-
self.app.builder,
83+
app.builder,
8384
self.env,
8485
)
8586

myst_nb/sphinx_.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from sphinx.util import logging as sphinx_logging
2626
from sphinx.util.docutils import SphinxTranslator
2727

28-
from myst_nb._compat import findall
28+
from myst_nb._compat import findall, get_env_app
2929
from myst_nb.core.config import NbParserConfig
3030
from myst_nb.core.execute import ExecutionResult, create_client
3131
from myst_nb.core.loggers import DEFAULT_LOG_TYPE, SphinxDocLogger
@@ -70,17 +70,20 @@ def parse(self, inputstring: str, document: nodes.document) -> None:
7070
:param inputstring: The source string to parse
7171
:param document: The root docutils node to add AST elements to
7272
"""
73-
assert self.env is not None, "env not set"
74-
document_path = self.env.doc2path(self.env.docname)
73+
env = getattr(document.settings, "env", None)
74+
if env is None:
75+
env = self.env
76+
assert env is not None, "env not set"
77+
document_path = env.doc2path(env.docname)
7578

7679
# get a logger for this document
7780
logger = SphinxDocLogger(document)
7881

7982
# get markdown parsing configuration
80-
md_config: MdParserConfig = self.env.myst_config
83+
md_config: MdParserConfig = env.myst_config
8184

8285
# get notebook rendering configuration
83-
nb_config: NbParserConfig = self.env.mystnb_config
86+
nb_config: NbParserConfig = env.mystnb_config
8487

8588
# create a reader for the notebook
8689
nb_reader = create_nb_reader(document_path, md_config, nb_config, inputstring)
@@ -159,13 +162,11 @@ def parse(self, inputstring: str, document: nodes.document) -> None:
159162

160163
# save final execution data
161164
if nb_client.exec_metadata:
162-
NbMetadataCollector.set_exec_data(
163-
self.env, self.env.docname, nb_client.exec_metadata
164-
)
165+
NbMetadataCollector.set_exec_data(env, env.docname, nb_client.exec_metadata)
165166
if nb_client.exec_metadata["traceback"]:
166167
# store error traceback in outdir and log its path
167-
reports_file = Path(self.env.app.outdir).joinpath(
168-
"reports", *(self.env.docname + ".err.log").split("/")
168+
reports_file = Path(get_env_app(env).outdir).joinpath(
169+
"reports", *(env.docname + ".err.log").split("/")
169170
)
170171
reports_file.parent.mkdir(parents=True, exist_ok=True)
171172
reports_file.write_text(
@@ -177,7 +178,7 @@ def parse(self, inputstring: str, document: nodes.document) -> None:
177178
)
178179

179180
# write final (updated) notebook to output folder (utf8 is standard encoding)
180-
path = self.env.docname.split("/")
181+
path = env.docname.split("/")
181182
ipynb_path = path[:-1] + [path[-1] + ".ipynb"]
182183
content = nbformat.writes(notebook).encode("utf-8")
183184
nb_renderer.write_file(ipynb_path, content, overwrite=True)
@@ -193,16 +194,14 @@ def parse(self, inputstring: str, document: nodes.document) -> None:
193194
overwrite=True,
194195
)
195196
NbMetadataCollector.set_doc_data(
196-
self.env, self.env.docname, "glue", list(nb_client.glue_data.keys())
197+
env, env.docname, "glue", list(nb_client.glue_data.keys())
197198
)
198199

199200
# move some document metadata to environment metadata,
200201
# so that we can later read it from the environment,
201202
# rather than having to load the whole doctree
202203
for key, (uri, kwargs) in document.attributes.pop("nb_js_files", {}).items():
203-
NbMetadataCollector.add_js_file(
204-
self.env, self.env.docname, key, uri, kwargs
205-
)
204+
NbMetadataCollector.add_js_file(env, env.docname, key, uri, kwargs)
206205

207206
# remove temporary state
208207
document.attributes.pop("nb_renderer")
@@ -325,7 +324,7 @@ def run(self, **kwargs: Any) -> None:
325324
"""Run the transform."""
326325
# get priority list for this builder
327326
# TODO allow for per-notebook/cell priority dicts?
328-
bname = self.app.builder.name
327+
bname = get_env_app(self.env).builder.name
329328
priority_list = get_mime_priority(
330329
bname, self.config["nb_mime_priority_overrides"]
331330
)

myst_nb/warnings_.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def create_warning(
9090
# otherwise we will use the configuration set by docutils
9191
suppress_warnings: Sequence[str] = []
9292
try:
93-
suppress_warnings = document.settings.env.app.config.suppress_warnings
93+
suppress_warnings = document.settings.env.config.suppress_warnings
9494
except AttributeError:
9595
suppress_warnings = document.settings.myst_suppress_warnings or []
9696
if _is_suppressed_warning(wtype, subtype.value, suppress_warnings):

pyproject.toml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ classifiers = [
1616
"Intended Audience :: Developers",
1717
"Programming Language :: Python :: 3",
1818
"Programming Language :: Python :: 3 :: Only",
19-
"Programming Language :: Python :: 3.9",
2019
"Programming Language :: Python :: 3.10",
2120
"Programming Language :: Python :: 3.11",
2221
"Programming Language :: Python :: 3.12",
@@ -34,7 +33,7 @@ keywords = [
3433
"docutils",
3534
"sphinx",
3635
]
37-
requires-python = ">=3.9"
36+
requires-python = ">=3.10"
3837
dependencies = [
3938
"importlib_metadata",
4039
"ipython",
@@ -43,7 +42,7 @@ dependencies = [
4342
"myst-parser>=1.0.0",
4443
"nbformat>=5.0",
4544
"pyyaml",
46-
"sphinx>=5",
45+
"sphinx>=5,<9", # If we get bugs for older versions, just pin this higher
4746
"typing-extensions",
4847
# ipykernel is not a requirement of the library,
4948
# but is a common requirement for users (registers the python3 kernel)
@@ -97,7 +96,7 @@ testing = [
9796
"ipywidgets>=8",
9897
"jupytext>=1.11.2",
9998
# Matplotlib outputs are sensitive to the matplotlib version
100-
"matplotlib==3.7.*",
99+
"matplotlib==3.10.*",
101100
"nbdime",
102101
"numpy",
103102
"pandas",
@@ -175,6 +174,10 @@ filterwarnings = [
175174
'ignore:datetime.datetime.utcnow\(\) is deprecated:DeprecationWarning:jupyter_cache',
176175
# From matplotlib’s dependencies
177176
'ignore::DeprecationWarning:pyparsing', # imports deprecated `sre_constants`
177+
# From myst-parser on Sphinx 9
178+
'ignore:.*MystReferenceResolver.app.*:DeprecationWarning',
179+
# Upstream myst-parser still hits env.app deprecations under Sphinx 9
180+
'ignore:.*env\\.app.*:DeprecationWarning:myst_parser',
178181
# Windows issues, some may need to be fixed in MyST-NB, others are upstream
179182
'ignore:Proactor event loop does not implement add_reader:RuntimeWarning:zmq',
180183
]

tests/conftest.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from sphinx.util.console import nocolor
2222

2323
pytest_plugins = "sphinx.testing.fixtures"
24+
LATEST_SPHINX_MAJOR = 9
2425

2526
# -Diff Configuration-#
2627
NB_VERSION = 4
@@ -100,7 +101,9 @@ def invalidate_files(self):
100101
def get_resolved_doctree(self, docname=None):
101102
"""Load and return the built docutils.document, after post-transforms."""
102103
docname = docname or self.files[0]
103-
doctree = self.env.get_and_resolve_doctree(docname, self.app.builder)
104+
doctree = self.env.get_and_resolve_doctree(
105+
docname, self.app.builder, tags=self.app.builder.tags
106+
)
104107
doctree["source"] = docname
105108
return doctree
106109

@@ -287,6 +290,9 @@ def _func(doctree):
287290
# alternatively the resolution of https://github.com/ESSS/pytest-regressions/issues/32
288291
@pytest.fixture()
289292
def file_regression(file_regression):
293+
# Only run regression tests on the latest Sphinx since minor document changes will cause these to break otherwise
294+
if sphinx_version_info[0] < LATEST_SPHINX_MAJOR:
295+
pytest.skip("file-regression checks run only on the latest Sphinx")
290296
return FileRegression(file_regression)
291297

292298

tests/test_execute/test_allow_errors_auto.ipynb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
"evalue": "oopsie!",
2020
"output_type": "error",
2121
"traceback": [
22-
"\u001B[0;31m---------------------------------------------------------------------------\u001B[0m",
23-
"\u001B[0;31mException\u001B[0m Traceback (most recent call last)",
24-
"Cell \u001B[0;32mIn[1], line 1\u001B[0m\n\u001B[0;32m----> 1\u001B[0m \u001B[38;5;28;01mraise\u001B[39;00m \u001B[38;5;167;01mException\u001B[39;00m(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124moopsie!\u001B[39m\u001B[38;5;124m\"\u001B[39m)\n",
25-
"\u001B[0;31mException\u001B[0m: oopsie!"
22+
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
23+
"\u001b[31mException\u001b[39m Traceback (most recent call last)",
24+
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[1]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m(\u001b[33m\"\u001b[39m\u001b[33moopsie!\u001b[39m\u001b[33m\"\u001b[39m)\n",
25+
"\u001b[31mException\u001b[39m: oopsie!"
2626
]
2727
}
2828
],
@@ -47,7 +47,7 @@
4747
"name": "python",
4848
"nbconvert_exporter": "python",
4949
"pygments_lexer": "ipython3",
50-
"version": "3.10.12"
50+
"version": "3.13.11"
5151
},
5252
"test_name": "notebook1"
5353
},

0 commit comments

Comments
 (0)