From 5d44b8dbdfd4ae446b4602e9886068c6ef0c8641 Mon Sep 17 00:00:00 2001 From: cherith <315844584+Cherith1222@users.noreply.github.com> Date: Thu, 8 Oct 2026 06:48:34 +0800 Subject: [PATCH 1/2] Accept sphinx-build -D boolean strings as bools. sphinx-build -D passes True as a string, and Sphinx only converts 0 and 1 for bool defaults. A cached override string is replaced as well. AI-assisted (Cursor). The account owner confirmed this can be submitted. --- myst_nb/sphinx_ext.py | 52 +++++++++++++++++++++++++++++++++- tests/test_cli_bool_config.py | 53 +++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 tests/test_cli_bool_config.py diff --git a/myst_nb/sphinx_ext.py b/myst_nb/sphinx_ext.py index 9a7b7996..930b1fd8 100644 --- a/myst_nb/sphinx_ext.py +++ b/myst_nb/sphinx_ext.py @@ -12,6 +12,7 @@ from myst_parser.sphinx_ext.main import setup_sphinx as setup_myst_parser from sphinx.application import Sphinx +from sphinx.config import Config from sphinx.util import logging as sphinx_logging from sphinx.util.fileutil import copy_asset_file @@ -39,6 +40,11 @@ # so we can tell if they have been set by a user, and warn them _UNSET = "--unset--" +# sphinx-build -D passes these strings. Sphinx itself only converts "0" and "1" +# when the option's default is a bool. +_CLI_BOOL_TRUE = frozenset({"1", "true", "yes", "on"}) +_CLI_BOOL_FALSE = frozenset({"0", "false", "no", "off"}) + def sphinx_setup(app: Sphinx): """Initialize Sphinx extension.""" @@ -71,6 +77,8 @@ def sphinx_setup(app: Sphinx): app.add_source_parser(Parser) app.add_source_suffix(".md", "myst-nb", override=True) app.add_source_suffix(".ipynb", "myst-nb") + # -D boolean strings must be real bools before Sphinx's 0/1 check (priority 800) + app.connect("config-inited", coerce_cli_bool_overrides, priority=100) # add additional file suffixes for parsing app.connect("config-inited", add_nb_custom_formats) # ensure notebook checkpoints are excluded from parsing @@ -123,6 +131,48 @@ def sphinx_setup(app: Sphinx): } +def _coerce_cli_bool(value: Any) -> Any: + """Return a real bool for the usual ``sphinx-build -D`` spellings.""" + if not isinstance(value, str): + return value + token = value.strip().lower() + if token in _CLI_BOOL_TRUE: + return True + if token in _CLI_BOOL_FALSE: + return False + return value + + +def coerce_cli_bool_overrides(_app: Sphinx, config: Config) -> None: + """Accept ``-D nb_execution_allow_errors=True`` as a boolean. + + ``sphinx-build -D`` supplies strings. These options are registered as + ``Any``, and a bool default is only converted from ``0`` and ``1``. + The string ``True`` would otherwise be rejected before notebook config + is built. ``False`` stays false. + """ + names: list[str] = [] + for name, _default, field in NbParserConfig().as_triple(): + if field.type is not bool or field.metadata.get("sphinx_exclude"): + continue + names.append(f"nb_{name}") + legacy_name = field.metadata.get("legacy_name") + if isinstance(legacy_name, str): + names.append(legacy_name) + overrides = config.overrides + for name in names: + if name not in overrides: + continue + original = overrides[name] + coerced = _coerce_cli_bool(original) + if coerced is original: + continue + overrides[name] = coerced + # Sphinx logs the config object before this handler, and that repr + # caches the original string. Writing the attribute replaces it. + config[name] = coerced + + def add_nb_custom_formats(app: Sphinx, config): """Add custom conversion formats.""" for suffix in config.nb_custom_formats: @@ -226,4 +276,4 @@ def add_per_page_html_resources( return js_files = NbMetadataCollector.get_js_files(cast(SphinxEnvType, app.env), pagename) for path, kwargs in js_files.values(): - app.add_js_file(path, **kwargs) # type: ignore[arg-type] + app.add_js_file(path, **kwargs) diff --git a/tests/test_cli_bool_config.py b/tests/test_cli_bool_config.py new file mode 100644 index 00000000..c5d16ef2 --- /dev/null +++ b/tests/test_cli_bool_config.py @@ -0,0 +1,53 @@ +"""Regression: sphinx-build -D boolean overrides are real bools.""" + +from pathlib import Path + +from sphinx.application import Sphinx + + +def _app_with_allow_errors(root: Path, allow_errors: str) -> Sphinx: + """Build a Sphinx app the way ``sphinx-build -D`` supplies a string.""" + src = root / "src" + src.mkdir(parents=True) + (src / "conf.py").write_text( + "extensions = ['myst_nb']\nnb_execution_mode = 'off'\n", + encoding="utf-8", + ) + (src / "index.md").write_text("# hi\n", encoding="utf-8") + return Sphinx( + srcdir=src, + confdir=src, + outdir=root / "out", + doctreedir=root / "doctree", + buildername="html", + confoverrides={"nb_execution_allow_errors": allow_errors}, + ) + + +def test_dash_d_allow_errors_string_is_bool(tmp_path: Path) -> None: + """``-D nb_execution_allow_errors=True`` is a bool, and ``False`` stays false.""" + true_app = _app_with_allow_errors(tmp_path / "true", "True") + assert true_app.env.mystnb_config.execution_allow_errors is True + + false_app = _app_with_allow_errors(tmp_path / "false", "False") + assert false_app.env.mystnb_config.execution_allow_errors is False + + +def test_dash_d_legacy_allow_errors_string_is_bool(tmp_path: Path) -> None: + """The deprecated ``execution_allow_errors`` override is a bool too.""" + src = tmp_path / "src" + src.mkdir() + (src / "conf.py").write_text( + "extensions = ['myst_nb']\nnb_execution_mode = 'off'\n", + encoding="utf-8", + ) + (src / "index.md").write_text("# hi\n", encoding="utf-8") + app = Sphinx( + srcdir=src, + confdir=src, + outdir=tmp_path / "out", + doctreedir=tmp_path / "doctree", + buildername="html", + confoverrides={"execution_allow_errors": "False"}, + ) + assert app.env.mystnb_config.execution_allow_errors is False From 6f07f10e8cc9b7b53b25b7434dc05db4e2e67735 Mon Sep 17 00:00:00 2001 From: cherith <315844584+Cherith1222@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:46:54 +0800 Subject: [PATCH 2/2] Keep notebook JavaScript options compatible with mypy. pre-commit checks myst_nb together, and Sphinx types add_js_file priority as int. Cast the stored string options so that check passes. AI-assisted (Cursor). --- myst_nb/sphinx_ext.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/myst_nb/sphinx_ext.py b/myst_nb/sphinx_ext.py index 930b1fd8..ecba1432 100644 --- a/myst_nb/sphinx_ext.py +++ b/myst_nb/sphinx_ext.py @@ -276,4 +276,6 @@ def add_per_page_html_resources( return js_files = NbMetadataCollector.get_js_files(cast(SphinxEnvType, app.env), pagename) for path, kwargs in js_files.values(): - app.add_js_file(path, **kwargs) + # Stored options are dict[str, str]. Sphinx types priority as int, so + # unpacking that dict fails when this module is checked with the collector. + app.add_js_file(path, **cast(dict[str, Any], kwargs))