From d5c7d6e20d387093ca9dbbba95ddd1c23be8cabe Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 2 Jul 2026 12:18:52 +0000 Subject: [PATCH 1/5] Phase 1: add shared test infrastructure (conftest, support, helpers) Consolidate duplicated _build_sphinx helpers, wheel constants, and conf templates into tests/support.py. Add shared assertion helpers and pytest fixtures without changing test behavior (41 tests still pass). Co-authored-by: chrizzftd --- tests/conftest.py | 81 ++++++++++++++++++++ tests/helpers.py | 64 ++++++++++++++++ tests/support.py | 80 +++++++++++++++++++ tests/test_asset_href.py | 40 ---------- tests/test_autodoc_bootstrap.py | 123 +++++++++--------------------- tests/test_autodoc_doctest.py | 69 +++-------------- tests/test_autodoc_include.py | 30 ++------ tests/test_build_replay.py | 91 ++-------------------- tests/test_local_wheel.py | 68 ++--------------- tests/test_nested_static_paths.py | 43 +---------- 10 files changed, 291 insertions(+), 398 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/helpers.py create mode 100644 tests/support.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..8f9f986 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,81 @@ +"""Shared pytest fixtures.""" + +from pathlib import Path + +import pytest + +from support import ( + FIXTURES, + WHEEL_NAME, + WHEEL_PATH, + build_sphinx, + copy_wheel_to, + pyrepl_conf_header, +) + + +@pytest.fixture +def html_builder(tmp_path): + """Sphinx HTML builder with index and nested api/module pages.""" + srcdir = tmp_path / "docs" + srcdir.mkdir() + (srcdir / "conf.py").write_text("", encoding="utf-8") + (srcdir / "index.rst").write_text("Test\n====\n", encoding="utf-8") + (srcdir / "api").mkdir() + (srcdir / "api" / "module.rst").write_text("API\n===\n", encoding="utf-8") + outdir = tmp_path / "_build" + doctreedir = tmp_path / "_doctree" + app = build_sphinx(srcdir, outdir, doctreedir) + return app.builder + + +@pytest.fixture +def wheel_paths(): + """Paths to the vendored test wheel fixture.""" + return FIXTURES / "wheels", WHEEL_NAME, WHEEL_PATH + + +@pytest.fixture +def sphinx_project(tmp_path): + """Minimal Sphinx project with a py-repl directive body.""" + srcdir = tmp_path / "docs" + srcdir.mkdir() + outdir = tmp_path / "_build" + doctreedir = tmp_path / "_doctree" + (srcdir / "conf.py").write_text( + pyrepl_conf_header(), + encoding="utf-8", + ) + (srcdir / "index.rst").write_text( + """ +Example +======= + +.. py-repl:: + :no-header: + + >>> x = 2 + 2 + >>> print(x) +""".strip() + + "\n", + encoding="utf-8", + ) + return srcdir, outdir, doctreedir + + +@pytest.fixture +def wheel_project(tmp_path): + """Sphinx project with the test wheel and bootstrap script in _static.""" + srcdir = tmp_path / "docs" + copy_wheel_to(srcdir) + (srcdir / "_static" / "bootstrap.py").write_text( + "# Optional post-install bootstrap for local wheel REPLs.\n", + encoding="utf-8", + ) + outdir = tmp_path / "_build" + doctreedir = tmp_path / "_doctree" + (srcdir / "conf.py").write_text( + pyrepl_conf_header(extra='html_static_path = ["_static"]\n'), + encoding="utf-8", + ) + return srcdir, outdir, doctreedir diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..45307ae --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,64 @@ +"""Shared assertion helpers for sphinx-pyrepl-web tests.""" + +import json +from pathlib import Path +from unittest.mock import MagicMock + +from sphinx.application import Sphinx + + +def mock_html_builder(docname: str = "api/module") -> MagicMock: + """Return a mock HTML builder whose target URIs match a nested page layout.""" + builder = MagicMock() + builder.format = "html" + + def get_target_uri(name: str) -> str: + if name == "index": + return "index.html" + return f"{name}.html" + + builder.get_target_uri.side_effect = get_target_uri + return builder + + +def pyrepl_tag(html: str) -> str: + """Extract the first ```` tag from HTML.""" + start = html.index("", start) + len(">") + return html[start:end] + + +def load_replay_files(app: Sphinx, docname: str) -> dict[str, str]: + """Load replay script metadata for a document.""" + return json.loads( + app.env.metadata[docname].get("pyrepl-replay-files", "{}") + ) + + +def load_bootstrap_files(app: Sphinx, docname: str) -> dict[str, str]: + """Load bootstrap script metadata for a document.""" + return json.loads( + app.env.metadata[docname].get("pyrepl-bootstrap-files", "{}") + ) + + +def assert_replay_artifacts( + app: Sphinx, + outdir: Path, + docname: str, + *, + count: int | None = None, + html: str | None = None, +) -> dict[str, str]: + """Assert replay metadata, on-disk scripts, and optional HTML references.""" + replay_files = load_replay_files(app, docname) + if count is not None: + assert len(replay_files) == count + + for script_name in replay_files: + script_path = outdir / "_static" / "pyrepl" / script_name + assert script_path.is_file(), f"missing replay script at {script_path}" + if html is not None: + assert f'replay-src="_static/pyrepl/{script_name}"' in html + + return replay_files diff --git a/tests/support.py b/tests/support.py new file mode 100644 index 0000000..53c5b5e --- /dev/null +++ b/tests/support.py @@ -0,0 +1,80 @@ +"""Shared constants and Sphinx project setup helpers.""" + +import shutil +import sys +from pathlib import Path + +from sphinx.application import Sphinx + +ROOT = Path(__file__).resolve().parents[1] +FIXTURES = Path(__file__).resolve().parent / "fixtures" +WHEEL_NAME = "pyrepl_test_pkg-1.0.0-py3-none-any.whl" +WHEEL_PATH = f"_static/wheels/{WHEEL_NAME}" + +sys.path.insert(0, str(ROOT)) + + +def build_sphinx( + srcdir: Path, + outdir: Path, + doctreedir: Path, + *, + parallel: int = 0, + **kwargs, +) -> Sphinx: + """Run a full Sphinx HTML build and return the application.""" + outdir.mkdir(parents=True, exist_ok=True) + doctreedir.mkdir(parents=True, exist_ok=True) + with open(outdir / "warnings.txt", "w", encoding="utf-8") as warning_file: + app = Sphinx( + srcdir=str(srcdir), + confdir=str(srcdir), + outdir=str(outdir), + doctreedir=str(doctreedir), + buildername="html", + warning=warning_file, + freshenv=True, + parallel=parallel, + **kwargs, + ) + app.build() + return app + + +def copy_wheel_to(srcdir: Path) -> Path: + """Copy the test wheel into ``srcdir/_static/wheels`` and return that directory.""" + wheels_dir = srcdir / "_static" / "wheels" + wheels_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(FIXTURES / "wheels" / WHEEL_NAME, wheels_dir / WHEEL_NAME) + return wheels_dir + + +def wheel_conf_extra() -> str: + """Return conf.py snippet enabling the test wheel for autodoc REPLs.""" + return f""" +html_static_path = ["_static"] +pyrepl_autodoc_packages = {WHEEL_PATH!r} +""" + + +def autodoc_conf_header(*, sys_path: str, extra: str = "") -> str: + """Return a minimal autodoc-enabled conf.py header.""" + return f""" +import sys +sys.path.insert(0, {sys_path!r}) +extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_pyrepl_web"] +master_doc = "index" +pyrepl_js = "pyrepl.js" +pyrepl_doctest_blocks = "autodoc" +{extra} +""" + + +def pyrepl_conf_header(*, extra: str = "") -> str: + """Return a minimal pyrepl-only conf.py header.""" + return f""" +extensions = ["sphinx_pyrepl_web"] +master_doc = "index" +pyrepl_js = "pyrepl.js" +{extra} +""" diff --git a/tests/test_asset_href.py b/tests/test_asset_href.py index fca2fb0..0f31970 100644 --- a/tests/test_asset_href.py +++ b/tests/test_asset_href.py @@ -1,47 +1,7 @@ -import shutil -import sys -from pathlib import Path from unittest.mock import MagicMock -import pytest -from sphinx.application import Sphinx - from sphinx_pyrepl_web import _asset_href, _asset_href_packages -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT)) - - -def _build_sphinx(srcdir: Path, outdir: Path, doctreedir: Path) -> Sphinx: - outdir.mkdir(parents=True, exist_ok=True) - doctreedir.mkdir(parents=True, exist_ok=True) - with open(outdir / "warnings.txt", "w", encoding="utf-8") as warning_file: - app = Sphinx( - srcdir=str(srcdir), - confdir=str(srcdir), - outdir=str(outdir), - doctreedir=str(doctreedir), - buildername="html", - warning=warning_file, - freshenv=True, - ) - app.build() - return app - - -@pytest.fixture -def html_builder(tmp_path): - srcdir = tmp_path / "docs" - srcdir.mkdir() - (srcdir / "conf.py").write_text("", encoding="utf-8") - (srcdir / "index.rst").write_text("Test\n====\n", encoding="utf-8") - (srcdir / "api").mkdir() - (srcdir / "api" / "module.rst").write_text("API\n===\n", encoding="utf-8") - outdir = tmp_path / "_build" - doctreedir = tmp_path / "_doctree" - app = _build_sphinx(srcdir, outdir, doctreedir) - return app.builder - def test_asset_href_rewrites_static_path_on_nested_page(html_builder): assert ( diff --git a/tests/test_autodoc_bootstrap.py b/tests/test_autodoc_bootstrap.py index c291a0a..f833d4c 100644 --- a/tests/test_autodoc_bootstrap.py +++ b/tests/test_autodoc_bootstrap.py @@ -1,64 +1,30 @@ -import json -import shutil -import sys -from pathlib import Path from unittest.mock import MagicMock -from sphinx.application import Sphinx +from helpers import load_bootstrap_files, load_replay_files, pyrepl_tag +from support import ( + FIXTURES, + WHEEL_NAME, + WHEEL_PATH, + autodoc_conf_header, + build_sphinx, + copy_wheel_to, + wheel_conf_extra, +) from sphinx_pyrepl_web import _autodoc_packages -ROOT = Path(__file__).resolve().parents[1] -FIXTURES = Path(__file__).resolve().parent / "fixtures" -WHEEL_NAME = "pyrepl_test_pkg-1.0.0-py3-none-any.whl" -WHEEL_PATH = f"_static/wheels/{WHEEL_NAME}" - -sys.path.insert(0, str(ROOT)) - - -def _build_sphinx(srcdir: Path, outdir: Path, doctreedir: Path) -> Sphinx: - outdir.mkdir(parents=True, exist_ok=True) - doctreedir.mkdir(parents=True, exist_ok=True) - with open(outdir / "warnings.txt", "w", encoding="utf-8") as warning_file: - app = Sphinx( - srcdir=str(srcdir), - confdir=str(srcdir), - outdir=str(outdir), - doctreedir=str(doctreedir), - buildername="html", - warning=warning_file, - freshenv=True, - ) - app.build() - return app - - -def _wheel_conf_extra() -> str: - return f""" -html_static_path = ["_static"] -pyrepl_autodoc_packages = {WHEEL_PATH!r} -""" - def test_autodoc_packages_emits_configured_wheel(tmp_path): - wheels_dir = tmp_path / "docs" / "_static" / "wheels" - wheels_dir.mkdir(parents=True) - shutil.copy2(FIXTURES / "wheels" / WHEEL_NAME, wheels_dir / WHEEL_NAME) - srcdir = tmp_path / "docs" + copy_wheel_to(srcdir) outdir = tmp_path / "_build" doctreedir = tmp_path / "_doctree" (srcdir / "conf.py").write_text( - f""" -import sys -sys.path.insert(0, {str(FIXTURES / "pyrepl_test_pkg")!r}) -extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_pyrepl_web"] -master_doc = "index" -pyrepl_js = "pyrepl.js" -pyrepl_doctest_blocks = "autodoc" -{_wheel_conf_extra()} -""", + autodoc_conf_header( + sys_path=str(FIXTURES / "pyrepl_test_pkg"), + extra=wheel_conf_extra(), + ), encoding="utf-8", ) (srcdir / "index.rst").write_text( @@ -66,29 +32,25 @@ def test_autodoc_packages_emits_configured_wheel(tmp_path): encoding="utf-8", ) - app = _build_sphinx(srcdir, outdir, doctreedir) + app = build_sphinx(srcdir, outdir, doctreedir) html = (outdir / "index.html").read_text(encoding="utf-8") assert f'packages="{WHEEL_PATH}"' in html assert 'replay-src="_static/pyrepl/index-1.py"' in html - pyrepl_tag = html[html.index("", html.index("")] - assert 'src="_static/pyrepl/index-1-bootstrap.py"' in pyrepl_tag + tag = pyrepl_tag(html) + assert 'src="_static/pyrepl/index-1-bootstrap.py"' in tag assert (outdir / "_static" / "wheels" / WHEEL_NAME).is_file() doctree = app.env.get_doctree("index") assert doctree.get("pyrepl") - replay_files = json.loads( - app.env.metadata["index"].get("pyrepl-replay-files", "{}") - ) + replay_files = load_replay_files(app, "index") assert list(replay_files) == ["index-1.py"] assert replay_files["index-1.py"] == ( "print([i for i in example_generator(4)])\n" ) - bootstrap_files = json.loads( - app.env.metadata["index"].get("pyrepl-bootstrap-files", "{}") - ) + bootstrap_files = load_bootstrap_files(app, "index") assert list(bootstrap_files) == ["index-1-bootstrap.py"] assert bootstrap_files["index-1-bootstrap.py"] == ( "from pyrepl_test_pkg.demo import example_generator\n" @@ -116,39 +78,29 @@ class Widget: encoding="utf-8", ) - wheels_dir = tmp_path / "docs" / "_static" / "wheels" - wheels_dir.mkdir(parents=True) - shutil.copy2(FIXTURES / "wheels" / WHEEL_NAME, wheels_dir / WHEEL_NAME) - srcdir = tmp_path / "docs" + copy_wheel_to(srcdir) outdir = tmp_path / "_build" doctreedir = tmp_path / "_doctree" (srcdir / "conf.py").write_text( - f""" -import sys -sys.path.insert(0, {str(pkg_dir.parent)!r}) -extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_pyrepl_web"] -master_doc = "index" -pyrepl_js = "pyrepl.js" -pyrepl_doctest_blocks = "autodoc" -{_wheel_conf_extra()} -""", + autodoc_conf_header( + sys_path=str(pkg_dir.parent), + extra=wheel_conf_extra(), + ), encoding="utf-8", ) (srcdir / "index.rst").write_text(".. autoclass:: installed_pkg.Widget\n", encoding="utf-8") - app = _build_sphinx(srcdir, outdir, doctreedir) + app = build_sphinx(srcdir, outdir, doctreedir) html = (outdir / "index.html").read_text(encoding="utf-8") assert f'packages="{WHEEL_PATH}"' in html assert 'replay-src="_static/pyrepl/index-1.py"' in html - pyrepl_tag = html[html.index("", html.index("")] - assert 'src="_static/pyrepl/index-1-bootstrap.py"' in pyrepl_tag + tag = pyrepl_tag(html) + assert 'src="_static/pyrepl/index-1-bootstrap.py"' in tag - bootstrap_files = json.loads( - app.env.metadata["index"].get("pyrepl-bootstrap-files", "{}") - ) + bootstrap_files = load_bootstrap_files(app, "index") assert bootstrap_files["index-1-bootstrap.py"] == "from installed_pkg import Widget\n" @@ -178,25 +130,18 @@ class Widget: doctreedir = tmp_path / "_doctree" (srcdir / "conf.py").write_text( - f""" -import sys -sys.path.insert(0, {str(pkg_dir.parent)!r}) -extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_pyrepl_web"] -master_doc = "index" -pyrepl_js = "pyrepl.js" -pyrepl_doctest_blocks = "autodoc" -""", + autodoc_conf_header(sys_path=str(pkg_dir.parent)), encoding="utf-8", ) (srcdir / "index.rst").write_text(".. autoclass:: installed_pkg.Widget\n", encoding="utf-8") - _build_sphinx(srcdir, outdir, doctreedir) + build_sphinx(srcdir, outdir, doctreedir) html = (outdir / "index.html").read_text(encoding="utf-8") assert 'replay-src="_static/pyrepl/index-1.py"' in html - pyrepl_tag = html[html.index("", html.index("")] - assert 'packages="' not in pyrepl_tag - assert ' src="' not in pyrepl_tag + tag = pyrepl_tag(html) + assert 'packages="' not in tag + assert ' src="' not in tag def test_autodoc_packages_config_helper(): diff --git a/tests/test_autodoc_doctest.py b/tests/test_autodoc_doctest.py index 1433396..1396c93 100644 --- a/tests/test_autodoc_doctest.py +++ b/tests/test_autodoc_doctest.py @@ -1,10 +1,7 @@ -import json -from pathlib import Path - import pytest -from sphinx.application import Sphinx -ROOT = Path(__file__).resolve().parents[1] +from helpers import assert_replay_artifacts, load_replay_files +from support import autodoc_conf_header, build_sphinx EXAMPLE_GENERATOR_SOURCE = ''' def example_generator(n): @@ -34,18 +31,7 @@ def autodoc_project(tmp_path): doctreedir = tmp_path / "_doctree" (srcdir / "conf.py").write_text( - f""" -import sys -sys.path.insert(0, {str(mod_dir)!r}) -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.napoleon", - "sphinx_pyrepl_web", -] -master_doc = "index" -pyrepl_js = "pyrepl.js" -pyrepl_doctest_blocks = "autodoc" -""", + autodoc_conf_header(sys_path=str(mod_dir)), encoding="utf-8", ) (srcdir / "index.rst").write_text( @@ -70,32 +56,11 @@ def autodoc_project(tmp_path): return srcdir, outdir, doctreedir, mod_dir -def _build_sphinx(srcdir, outdir, doctreedir, **kwargs): - outdir.mkdir(parents=True, exist_ok=True) - doctreedir.mkdir(parents=True, exist_ok=True) - with open(outdir / "warnings.txt", "w", encoding="utf-8") as warning_file: - app = Sphinx( - srcdir=str(srcdir), - confdir=str(srcdir), - outdir=str(outdir), - doctreedir=str(doctreedir), - buildername="html", - warning=warning_file, - freshenv=True, - **kwargs, - ) - app.build() - return app - - def test_autodoc_doctest_becomes_pyrepl(autodoc_project): srcdir, outdir, doctreedir, _ = autodoc_project - app = _build_sphinx(srcdir, outdir, doctreedir) + app = build_sphinx(srcdir, outdir, doctreedir) - replay_files = json.loads( - app.env.metadata["index"].get("pyrepl-replay-files", "{}") - ) - assert len(replay_files) == 1 + replay_files = assert_replay_artifacts(app, outdir, "index", count=1) script_name = next(iter(replay_files)) script = replay_files[script_name] assert script == "print([i for i in example_generator(4)])\n" @@ -107,18 +72,12 @@ def test_autodoc_doctest_becomes_pyrepl(autodoc_project): assert "no-header" in html assert "no-banner" in html - script_path = outdir / "_static" / "pyrepl" / script_name - assert script_path.is_file(), f"missing replay script at {script_path}" - def test_autodoc_scope_skips_plain_rst_doctest(autodoc_project): srcdir, outdir, doctreedir, _ = autodoc_project - app = _build_sphinx(srcdir, outdir, doctreedir) + app = build_sphinx(srcdir, outdir, doctreedir) - replay_files = json.loads( - app.env.metadata["index"].get("pyrepl-replay-files", "{}") - ) - assert len(replay_files) == 1 + assert len(load_replay_files(app, "index")) == 1 html = (outdir / "index.html").read_text(encoding="utf-8") assert html.count("replay-src=") == 1 @@ -130,12 +89,9 @@ def test_all_scope_transforms_plain_rst_doctest(autodoc_project): (srcdir / "conf.py").write_text( conf + '\npyrepl_doctest_blocks = "all"\n', encoding="utf-8" ) - app = _build_sphinx(srcdir, outdir, doctreedir) + app = build_sphinx(srcdir, outdir, doctreedir) - replay_files = json.loads( - app.env.metadata["index"].get("pyrepl-replay-files", "{}") - ) - assert len(replay_files) == 2 + assert len(load_replay_files(app, "index")) == 2 html = (outdir / "index.html").read_text(encoding="utf-8") assert html.count("replay-src=") == 2 @@ -147,12 +103,9 @@ def test_default_scope_leaves_doctest_static(autodoc_project): (srcdir / "conf.py").write_text( conf.replace('pyrepl_doctest_blocks = "autodoc"\n', ""), encoding="utf-8" ) - app = _build_sphinx(srcdir, outdir, doctreedir) + app = build_sphinx(srcdir, outdir, doctreedir) - replay_files = json.loads( - app.env.metadata["index"].get("pyrepl-replay-files", "{}") - ) - assert replay_files == {} + assert load_replay_files(app, "index") == {} html = (outdir / "index.html").read_text(encoding="utf-8") assert "replay-src=" not in html diff --git a/tests/test_autodoc_include.py b/tests/test_autodoc_include.py index 500229c..7a7011b 100644 --- a/tests/test_autodoc_include.py +++ b/tests/test_autodoc_include.py @@ -1,12 +1,7 @@ -import json -import sys -from pathlib import Path - import pytest -from sphinx.application import Sphinx -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT)) +from helpers import assert_replay_artifacts +from support import build_sphinx @pytest.fixture @@ -18,7 +13,7 @@ def included_example_project(tmp_path): doctreedir = tmp_path / "_doctree" (srcdir / "conf.py").write_text( - f""" + """ import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent / "_static")) @@ -71,24 +66,9 @@ def example_generator(n): def test_included_example_writes_all_replay_scripts(included_example_project): srcdir, outdir, doctreedir = included_example_project - outdir.mkdir(parents=True, exist_ok=True) - doctreedir.mkdir(parents=True, exist_ok=True) - with open(outdir / "warnings.txt", "w", encoding="utf-8") as warning_file: - app = Sphinx( - srcdir=str(srcdir), - confdir=str(srcdir), - outdir=str(outdir), - doctreedir=str(doctreedir), - buildername="html", - warning=warning_file, - freshenv=True, - ) - app.build() + app = build_sphinx(srcdir, outdir, doctreedir) - replay_files = json.loads( - app.env.metadata["index"].get("pyrepl-replay-files", "{}") - ) - assert len(replay_files) == 2 + replay_files = assert_replay_artifacts(app, outdir, "index", count=2) html = (outdir / "index.html").read_text(encoding="utf-8") assert html.count("replay-src=") == 2 diff --git a/tests/test_build_replay.py b/tests/test_build_replay.py index 622bb94..ab3fa66 100644 --- a/tests/test_build_replay.py +++ b/tests/test_build_replay.py @@ -1,64 +1,14 @@ -import json -import sys -from pathlib import Path - -import pytest -from sphinx.application import Sphinx - -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT)) - - -@pytest.fixture -def sphinx_project(tmp_path): - srcdir = tmp_path / "docs" - srcdir.mkdir() - outdir = tmp_path / "_build" - doctreedir = tmp_path / "_doctree" - (srcdir / "conf.py").write_text( - "extensions = ['sphinx_pyrepl_web']\n" - "pyrepl_js = 'pyrepl.js'\n" - "master_doc = 'index'\n", - encoding="utf-8", - ) - (srcdir / "index.rst").write_text( - """ -Example -======= - -.. py-repl:: - :no-header: - - >>> x = 2 + 2 - >>> print(x) -""".strip() - + "\n", - encoding="utf-8", - ) - return srcdir, outdir, doctreedir +from helpers import assert_replay_artifacts +from support import build_sphinx, pyrepl_conf_header def test_build_writes_replay_script_from_metadata(sphinx_project): srcdir, outdir, doctreedir = sphinx_project - outdir.mkdir(parents=True, exist_ok=True) - doctreedir.mkdir(parents=True, exist_ok=True) - with open(outdir / "warnings.txt", "w", encoding="utf-8") as warning_file: - app = Sphinx( - srcdir=str(srcdir), - confdir=str(srcdir), - outdir=str(outdir), - doctreedir=str(doctreedir), - buildername="html", - warning=warning_file, - freshenv=True, - ) - app.build() + app = build_sphinx(srcdir, outdir, doctreedir) - replay_files = json.loads(app.env.metadata["index"].get("pyrepl-replay-files", "{}")) - assert replay_files + replay_files = assert_replay_artifacts(app, outdir, "index") script_name = next(iter(replay_files)) script_path = outdir / "_static" / "pyrepl" / script_name - assert script_path.is_file(), f"missing replay script at {script_path}" assert "x = 2 + 2" in script_path.read_text(encoding="utf-8") html = (outdir / "index.html").read_text(encoding="utf-8") @@ -67,20 +17,7 @@ def test_build_writes_replay_script_from_metadata(sphinx_project): def test_build_writes_replay_script_with_parallel_read(sphinx_project): srcdir, outdir, doctreedir = sphinx_project - outdir.mkdir(parents=True, exist_ok=True) - doctreedir.mkdir(parents=True, exist_ok=True) - with open(outdir / "warnings.txt", "w", encoding="utf-8") as warning_file: - app = Sphinx( - srcdir=str(srcdir), - confdir=str(srcdir), - outdir=str(outdir), - doctreedir=str(doctreedir), - buildername="html", - warning=warning_file, - freshenv=True, - parallel=2, - ) - app.build() + build_sphinx(srcdir, outdir, doctreedir, parallel=2) script_path = outdir / "_static" / "pyrepl" / "index-1.py" assert script_path.is_file(), f"missing replay script at {script_path}" @@ -92,9 +29,7 @@ def test_build_omits_bare_doctest_terminator(tmp_path): outdir = tmp_path / "_build" doctreedir = tmp_path / "_doctree" (srcdir / "conf.py").write_text( - "extensions = ['sphinx_pyrepl_web']\n" - "pyrepl_js = 'pyrepl.js'\n" - "master_doc = 'index'\n", + pyrepl_conf_header(), encoding="utf-8", ) (srcdir / "index.rst").write_text( @@ -113,19 +48,7 @@ def test_build_omits_bare_doctest_terminator(tmp_path): + "\n", encoding="utf-8", ) - outdir.mkdir(parents=True, exist_ok=True) - doctreedir.mkdir(parents=True, exist_ok=True) - with open(outdir / "warnings.txt", "w", encoding="utf-8") as warning_file: - app = Sphinx( - srcdir=str(srcdir), - confdir=str(srcdir), - outdir=str(outdir), - doctreedir=str(doctreedir), - buildername="html", - warning=warning_file, - freshenv=True, - ) - app.build() + build_sphinx(srcdir, outdir, doctreedir) script_path = outdir / "_static" / "pyrepl" / "index-1.py" script = script_path.read_text(encoding="utf-8") diff --git a/tests/test_local_wheel.py b/tests/test_local_wheel.py index 8cb19f4..157f0c4 100644 --- a/tests/test_local_wheel.py +++ b/tests/test_local_wheel.py @@ -1,61 +1,7 @@ -import json -import shutil -import sys -from pathlib import Path - -import pytest -from sphinx.application import Sphinx from sphinx_pytest.plugin import CreateDoctree -ROOT = Path(__file__).resolve().parents[1] -FIXTURES = Path(__file__).resolve().parent / "fixtures" -WHEEL_NAME = "pyrepl_test_pkg-1.0.0-py3-none-any.whl" -WHEEL_PATH = f"_static/wheels/{WHEEL_NAME}" - -sys.path.insert(0, str(ROOT)) - - -def _build_sphinx(srcdir: Path, outdir: Path, doctreedir: Path, **kwargs) -> Sphinx: - outdir.mkdir(parents=True, exist_ok=True) - doctreedir.mkdir(parents=True, exist_ok=True) - with open(outdir / "warnings.txt", "w", encoding="utf-8") as warning_file: - app = Sphinx( - srcdir=str(srcdir), - confdir=str(srcdir), - outdir=str(outdir), - doctreedir=str(doctreedir), - buildername="html", - warning=warning_file, - freshenv=True, - **kwargs, - ) - app.build() - return app - - -@pytest.fixture -def wheel_project(tmp_path): - srcdir = tmp_path / "docs" - wheels_dir = srcdir / "_static" / "wheels" - wheels_dir.mkdir(parents=True) - shutil.copy2(FIXTURES / "wheels" / WHEEL_NAME, wheels_dir / WHEEL_NAME) - (srcdir / "_static" / "bootstrap.py").write_text( - "# Optional post-install bootstrap for local wheel REPLs.\n", - encoding="utf-8", - ) - - outdir = tmp_path / "_build" - doctreedir = tmp_path / "_doctree" - (srcdir / "conf.py").write_text( - """ -extensions = ["sphinx_pyrepl_web"] -master_doc = "index" -pyrepl_js = "pyrepl.js" -html_static_path = ["_static"] -""", - encoding="utf-8", - ) - return srcdir, outdir, doctreedir +from helpers import assert_replay_artifacts, load_replay_files +from support import WHEEL_NAME, WHEEL_PATH, build_sphinx def test_local_wheel_packages_emitted_in_doctree(sphinx_doctree: CreateDoctree): @@ -86,7 +32,7 @@ def test_local_wheel_copied_to_output_tree(wheel_project): encoding="utf-8", ) - _build_sphinx(srcdir, outdir, doctreedir) + build_sphinx(srcdir, outdir, doctreedir) wheel_out = outdir / "_static" / "wheels" / WHEEL_NAME assert wheel_out.is_file(), f"missing wheel at {wheel_out}" @@ -115,17 +61,13 @@ def test_local_wheel_with_src_and_replay_body(wheel_project): encoding="utf-8", ) - app = _build_sphinx(srcdir, outdir, doctreedir) + app = build_sphinx(srcdir, outdir, doctreedir) assert (outdir / "_static" / "wheels" / WHEEL_NAME).is_file() assert (outdir / "_static" / "bootstrap.py").is_file() - replay_files = json.loads( - app.env.metadata["index"].get("pyrepl-replay-files", "{}") - ) - assert len(replay_files) == 1 + replay_files = assert_replay_artifacts(app, outdir, "index", count=1) script_name = next(iter(replay_files)) - assert (outdir / "_static" / "pyrepl" / script_name).is_file() html = (outdir / "index.html").read_text(encoding="utf-8") assert f'packages="{WHEEL_PATH}"' in html diff --git a/tests/test_nested_static_paths.py b/tests/test_nested_static_paths.py index 2acc8cc..d443abc 100644 --- a/tests/test_nested_static_paths.py +++ b/tests/test_nested_static_paths.py @@ -1,50 +1,15 @@ -import shutil -import sys -from pathlib import Path - -from sphinx.application import Sphinx - -ROOT = Path(__file__).resolve().parents[1] -FIXTURES = Path(__file__).resolve().parent / "fixtures" -WHEEL_NAME = "pyrepl_test_pkg-1.0.0-py3-none-any.whl" -WHEEL_PATH = f"_static/wheels/{WHEEL_NAME}" - -sys.path.insert(0, str(ROOT)) - - -def _build_sphinx(srcdir: Path, outdir: Path, doctreedir: Path) -> Sphinx: - outdir.mkdir(parents=True, exist_ok=True) - doctreedir.mkdir(parents=True, exist_ok=True) - with open(outdir / "warnings.txt", "w", encoding="utf-8") as warning_file: - app = Sphinx( - srcdir=str(srcdir), - confdir=str(srcdir), - outdir=str(outdir), - doctreedir=str(doctreedir), - buildername="html", - warning=warning_file, - freshenv=True, - ) - app.build() - return app +from support import WHEEL_PATH, build_sphinx, copy_wheel_to, pyrepl_conf_header def test_nested_page_emits_page_relative_static_paths(tmp_path): srcdir = tmp_path / "docs" - wheels_dir = srcdir / "_static" / "wheels" - wheels_dir.mkdir(parents=True) - shutil.copy2(FIXTURES / "wheels" / WHEEL_NAME, wheels_dir / WHEEL_NAME) + copy_wheel_to(srcdir) (srcdir / "api").mkdir() outdir = tmp_path / "_build" doctreedir = tmp_path / "_doctree" (srcdir / "conf.py").write_text( - """ -extensions = ["sphinx_pyrepl_web"] -master_doc = "index" -pyrepl_js = "pyrepl.js" -html_static_path = ["_static"] -""", + pyrepl_conf_header(extra='html_static_path = ["_static"]\n'), encoding="utf-8", ) (srcdir / "index.rst").write_text("Home\n====\n", encoding="utf-8") @@ -62,7 +27,7 @@ def test_nested_page_emits_page_relative_static_paths(tmp_path): encoding="utf-8", ) - app = _build_sphinx(srcdir, outdir, doctreedir) + app = build_sphinx(srcdir, outdir, doctreedir) html = (outdir / "api" / "index.html").read_text(encoding="utf-8") assert 'packages="../_static/wheels/' in html From 7205fcb13bbd4f5eda81a1abc0bc0c53aa01fa29 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 2 Jul 2026 12:20:13 +0000 Subject: [PATCH 2/5] Phase 2: parametrize unit tests and extract test_config Move unit tests into tests/unit/ with parametrized cases. Replace the html_builder fixture in asset_href tests with mock_html_builder for faster isolated checks. Extract _autodoc_packages tests into test_config.py. Co-authored-by: chrizzftd --- tests/doctree/test_autodoc_scope.py | 72 +++++++++++++++++++++ tests/doctree/test_pyrepl_directive.py | 52 +++++++++++++++ tests/fixtures/sources.py | 45 +++++++++++++ tests/test_asset_href.py | 58 ----------------- tests/test_autodoc_bootstrap.py | 16 ----- tests/test_autodoc_bootstrap_source.py | 29 --------- tests/test_doctest_to_replay_source.py | 55 ---------------- tests/unit/test_asset_href.py | 53 +++++++++++++++ tests/unit/test_autodoc_bootstrap_source.py | 38 +++++++++++ tests/unit/test_config.py | 20 ++++++ tests/unit/test_doctest_to_replay_source.py | 64 ++++++++++++++++++ 11 files changed, 344 insertions(+), 158 deletions(-) create mode 100644 tests/doctree/test_autodoc_scope.py create mode 100644 tests/doctree/test_pyrepl_directive.py create mode 100644 tests/fixtures/sources.py delete mode 100644 tests/test_asset_href.py delete mode 100644 tests/test_autodoc_bootstrap_source.py delete mode 100644 tests/test_doctest_to_replay_source.py create mode 100644 tests/unit/test_asset_href.py create mode 100644 tests/unit/test_autodoc_bootstrap_source.py create mode 100644 tests/unit/test_config.py create mode 100644 tests/unit/test_doctest_to_replay_source.py diff --git a/tests/doctree/test_autodoc_scope.py b/tests/doctree/test_autodoc_scope.py new file mode 100644 index 0000000..a2db0d7 --- /dev/null +++ b/tests/doctree/test_autodoc_scope.py @@ -0,0 +1,72 @@ +import pytest +from sphinx_pytest.plugin import CreateDoctree + +from fixtures.sources import AUTODOC_SCOPE_RST, EXAMPLE_GENERATOR_SOURCE + + +@pytest.fixture +def autodoc_doctree(sphinx_doctree: CreateDoctree): + """Configure sphinx_doctree with an autodoc module and doctest scope RST.""" + (sphinx_doctree.srcdir / "repl_test_demo.py").write_text( + EXAMPLE_GENERATOR_SOURCE, + encoding="utf-8", + ) + sphinx_doctree.set_conf( + { + "extensions": [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx_pyrepl_web", + ], + "pyrepl_doctest_blocks": "autodoc", + "pyrepl_js": "pyrepl.js", + } + ) + sphinx_doctree.buildername = "html" + return sphinx_doctree + + +def count_pyrepl_nodes(doctree) -> int: + """Return the number of py-repl raw HTML nodes in a doctree.""" + from docutils import nodes + + count = 0 + for node in doctree.traverse(nodes.raw): + text = node.astext() + if " +
+ + Test + <raw format="html" xml:space="preserve"> + <py-repl theme="catppuccin-latte" no-header></py-repl> + <raw format="html" xml:space="preserve"> + <py-repl></py-repl> + """.strip().splitlines() + + +def test_replay_file_flag(sphinx_doctree: CreateDoctree): + sphinx_doctree.set_conf( + { + "extensions": ["sphinx_pyrepl_web"], + "root_doc": "index", + } + ) + sphinx_doctree.buildername = "html" + (sphinx_doctree.srcdir / "demo.py").write_text("print('hi')\n", encoding="utf-8") + result = sphinx_doctree( + """ +.. py-repl:: + :src: demo.py + :replay: + """ + ) + html = result.pformat() + assert 'src="demo.py"' in html + assert "replay" in html diff --git a/tests/fixtures/sources.py b/tests/fixtures/sources.py new file mode 100644 index 0000000..51600d1 --- /dev/null +++ b/tests/fixtures/sources.py @@ -0,0 +1,45 @@ +"""Shared RST snippets and inline module sources for doctree tests.""" + +EXAMPLE_GENERATOR_SOURCE = ''' +def example_generator(n): + """Generators have a ``Yields`` section instead of a ``Returns`` section. + + Examples: + Examples should be written in doctest format, and should illustrate how + to use the function. + + >>> print([i for i in example_generator(4)]) + [0, 1, 2, 3] + + """ + yield from range(n) +'''.strip() + "\n" + +AUTODOC_SCOPE_RST = """ +API +=== + +.. autofunction:: repl_test_demo.example_generator + +Tutorial +======== + +Plain doctest (outside autodoc): + +>>> x = 1 +>>> x + 1 +2 +""".strip() + +WIDGET_SOURCE = ''' +class Widget: + """A demo widget. + + Example: + >>> w = Widget() + >>> w.label + 'ready' + + """ + label = "ready" +'''.strip() + "\n" diff --git a/tests/test_asset_href.py b/tests/test_asset_href.py deleted file mode 100644 index 0f31970..0000000 --- a/tests/test_asset_href.py +++ /dev/null @@ -1,58 +0,0 @@ -from unittest.mock import MagicMock - -from sphinx_pyrepl_web import _asset_href, _asset_href_packages - - -def test_asset_href_rewrites_static_path_on_nested_page(html_builder): - assert ( - _asset_href(html_builder, "api/module", "_static/wheels/foo.whl") - == "../_static/wheels/foo.whl" - ) - - -def test_asset_href_leaves_root_page_static_path_unchanged(html_builder): - assert ( - _asset_href(html_builder, "index", "_static/wheels/foo.whl") - == "_static/wheels/foo.whl" - ) - - -def test_asset_href_leaves_root_absolute_path_unchanged(html_builder): - assert ( - _asset_href(html_builder, "api/module", "/_static/wheels/foo.whl") - == "/_static/wheels/foo.whl" - ) - - -def test_asset_href_leaves_https_url_unchanged(html_builder): - url = "https://cdn.example/w.whl" - assert _asset_href(html_builder, "api/module", url) == url - - -def test_asset_href_leaves_pypi_name_unchanged(html_builder): - assert _asset_href(html_builder, "api/module", "numpy") == "numpy" - - -def test_asset_href_rewrites_src_relative_to_source_root(html_builder): - assert _asset_href(html_builder, "api/module", "demo.py") == "../demo.py" - - -def test_asset_href_leaves_micropip_spec_unchanged(html_builder): - spec = "mypkg @ https://example.com/wheels/mypkg.whl" - assert _asset_href(html_builder, "api/module", spec) == spec - - -def test_asset_href_packages_rewrites_only_file_like_entries(html_builder): - packages = "numpy, _static/wheels/foo.whl" - assert _asset_href_packages(html_builder, "api/module", packages) == ( - "numpy, ../_static/wheels/foo.whl" - ) - - -def test_asset_href_skips_normalization_for_non_html_builder(): - builder = MagicMock() - builder.format = "" - assert ( - _asset_href(builder, "api/module", "_static/wheels/foo.whl") - == "_static/wheels/foo.whl" - ) diff --git a/tests/test_autodoc_bootstrap.py b/tests/test_autodoc_bootstrap.py index f833d4c..bc4098e 100644 --- a/tests/test_autodoc_bootstrap.py +++ b/tests/test_autodoc_bootstrap.py @@ -1,5 +1,3 @@ -from unittest.mock import MagicMock - from helpers import load_bootstrap_files, load_replay_files, pyrepl_tag from support import ( FIXTURES, @@ -11,8 +9,6 @@ wheel_conf_extra, ) -from sphinx_pyrepl_web import _autodoc_packages - def test_autodoc_packages_emits_configured_wheel(tmp_path): srcdir = tmp_path / "docs" @@ -142,15 +138,3 @@ class Widget: tag = pyrepl_tag(html) assert 'packages="' not in tag assert ' src="' not in tag - - -def test_autodoc_packages_config_helper(): - app = MagicMock() - app.config.pyrepl_autodoc_packages = WHEEL_PATH - assert _autodoc_packages(app) == WHEEL_PATH - - app.config.pyrepl_autodoc_packages = "" - assert _autodoc_packages(app) is None - - app.config.pyrepl_autodoc_packages = None - assert _autodoc_packages(app) is None diff --git a/tests/test_autodoc_bootstrap_source.py b/tests/test_autodoc_bootstrap_source.py deleted file mode 100644 index 4a0e35b..0000000 --- a/tests/test_autodoc_bootstrap_source.py +++ /dev/null @@ -1,29 +0,0 @@ -from sphinx_pyrepl_web import autodoc_bootstrap_source - - -def test_function_import(): - assert autodoc_bootstrap_source( - "pyrepl_test_pkg.demo", "example_generator", "function" - ) == "from pyrepl_test_pkg.demo import example_generator\n" - - -def test_class_import(): - assert autodoc_bootstrap_source("installed_pkg", "Widget", "class") == ( - "from installed_pkg import Widget\n" - ) - - -def test_method_imports_outer_class(): - assert autodoc_bootstrap_source("pkg.mod", "Widget.label", "method") == ( - "from pkg.mod import Widget\n" - ) - - -def test_module_import(): - assert autodoc_bootstrap_source("pyrepl_test_pkg.demo", "", "module") == ( - "import pyrepl_test_pkg.demo\n" - ) - - -def test_missing_module_returns_none(): - assert autodoc_bootstrap_source(None, "foo", "function") is None diff --git a/tests/test_doctest_to_replay_source.py b/tests/test_doctest_to_replay_source.py deleted file mode 100644 index d1fb725..0000000 --- a/tests/test_doctest_to_replay_source.py +++ /dev/null @@ -1,55 +0,0 @@ -from sphinx_pyrepl_web import doctest_to_replay_source - - -def test_strips_expected_output(): - block = ">>> add(1, 2)\n3\n>>> add(0, 0)\n0" - assert doctest_to_replay_source(block) == "add(1, 2)\n\nadd(0, 0)\n" - - -def test_example_generator(): - block = ">>> print([i for i in example_generator(4)])\n[0, 1, 2, 3]" - assert doctest_to_replay_source(block) == ( - "print([i for i in example_generator(4)])\n" - ) - - -def test_multiline_class(): - block = ">>> class Foo:\n... x = 1\n...\n>>> Foo()\n<Foo object>" - assert doctest_to_replay_source(block) == "class Foo:\n x = 1\n\nFoo()\n" - - -def test_name_class_example(): - block = ( - ">>> from naming import Name\n" - ">>> class MyName(Name):\n" - "... config = dict(base=r'\\w+')\n" - "...\n" - ">>> n = MyName()\n" - "'{base}'" - ) - assert doctest_to_replay_source(block) == ( - "from naming import Name\n" - "\n" - "class MyName(Name):\n" - " config = dict(base=r'\\w+')\n" - "\n" - "n = MyName()\n" - ) - - -def test_from_directive_lines(): - lines = [ - ">>> class Foo:", - "... x = 1", - "...", - ">>> Foo()", - ] - assert doctest_to_replay_source(lines) == "class Foo:\n x = 1\n\nFoo()\n" - - -def test_empty_for_non_doctest(): - assert doctest_to_replay_source("not a doctest") == "" - - -def test_explicit_ellipsis(): - assert doctest_to_replay_source([">>> ..."]) == "...\n" diff --git a/tests/unit/test_asset_href.py b/tests/unit/test_asset_href.py new file mode 100644 index 0000000..afb9599 --- /dev/null +++ b/tests/unit/test_asset_href.py @@ -0,0 +1,53 @@ +from unittest.mock import MagicMock + +import pytest + +from helpers import mock_html_builder +from sphinx_pyrepl_web import _asset_href, _asset_href_packages + + +@pytest.mark.parametrize( + ("docname", "path", "expected"), + [ + ("api/module", "_static/wheels/foo.whl", "../_static/wheels/foo.whl"), + ("index", "_static/wheels/foo.whl", "_static/wheels/foo.whl"), + ("api/module", "/_static/wheels/foo.whl", "/_static/wheels/foo.whl"), + ("api/module", "https://cdn.example/w.whl", "https://cdn.example/w.whl"), + ("api/module", "numpy", "numpy"), + ("api/module", "demo.py", "../demo.py"), + ( + "api/module", + "mypkg @ https://example.com/wheels/mypkg.whl", + "mypkg @ https://example.com/wheels/mypkg.whl", + ), + ], + ids=[ + "nested-static", + "root-static", + "root-absolute", + "https-url", + "pypi-name", + "src-relative", + "micropip-spec", + ], +) +def test_asset_href(docname, path, expected): + builder = mock_html_builder() + assert _asset_href(builder, docname, path) == expected + + +def test_asset_href_packages_rewrites_only_file_like_entries(): + builder = mock_html_builder() + packages = "numpy, _static/wheels/foo.whl" + assert _asset_href_packages(builder, "api/module", packages) == ( + "numpy, ../_static/wheels/foo.whl" + ) + + +def test_asset_href_skips_normalization_for_non_html_builder(): + builder = MagicMock() + builder.format = "" + assert ( + _asset_href(builder, "api/module", "_static/wheels/foo.whl") + == "_static/wheels/foo.whl" + ) diff --git a/tests/unit/test_autodoc_bootstrap_source.py b/tests/unit/test_autodoc_bootstrap_source.py new file mode 100644 index 0000000..32c4ca6 --- /dev/null +++ b/tests/unit/test_autodoc_bootstrap_source.py @@ -0,0 +1,38 @@ +import pytest + +from sphinx_pyrepl_web import autodoc_bootstrap_source + + +@pytest.mark.parametrize( + ("module", "fullname", "objtype", "expected"), + [ + ( + "pyrepl_test_pkg.demo", + "example_generator", + "function", + "from pyrepl_test_pkg.demo import example_generator\n", + ), + ( + "installed_pkg", + "Widget", + "class", + "from installed_pkg import Widget\n", + ), + ( + "pkg.mod", + "Widget.label", + "method", + "from pkg.mod import Widget\n", + ), + ( + "pyrepl_test_pkg.demo", + "", + "module", + "import pyrepl_test_pkg.demo\n", + ), + (None, "foo", "function", None), + ], + ids=["function", "class", "method", "module", "missing-module"], +) +def test_autodoc_bootstrap_source(module, fullname, objtype, expected): + assert autodoc_bootstrap_source(module, fullname, objtype) == expected diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py new file mode 100644 index 0000000..f0b9d39 --- /dev/null +++ b/tests/unit/test_config.py @@ -0,0 +1,20 @@ +import pytest +from unittest.mock import MagicMock + +from support import WHEEL_PATH +from sphinx_pyrepl_web import _autodoc_packages + + +@pytest.mark.parametrize( + ("configured", "expected"), + [ + (WHEEL_PATH, WHEEL_PATH), + ("", None), + (None, None), + ], + ids=["wheel-path", "empty-string", "none"], +) +def test_autodoc_packages(configured, expected): + app = MagicMock() + app.config.pyrepl_autodoc_packages = configured + assert _autodoc_packages(app) == expected diff --git a/tests/unit/test_doctest_to_replay_source.py b/tests/unit/test_doctest_to_replay_source.py new file mode 100644 index 0000000..bd9a5ec --- /dev/null +++ b/tests/unit/test_doctest_to_replay_source.py @@ -0,0 +1,64 @@ +import pytest + +from sphinx_pyrepl_web import doctest_to_replay_source + +MULTILINE_CLASS_EXPECTED = "class Foo:\n x = 1\n\nFoo()\n" + + +@pytest.mark.parametrize( + ("source", "expected"), + [ + ( + ">>> add(1, 2)\n3\n>>> add(0, 0)\n0", + "add(1, 2)\n\nadd(0, 0)\n", + ), + ( + ">>> print([i for i in example_generator(4)])\n[0, 1, 2, 3]", + "print([i for i in example_generator(4)])\n", + ), + ( + ">>> class Foo:\n... x = 1\n...\n>>> Foo()\n<Foo object>", + MULTILINE_CLASS_EXPECTED, + ), + ( + ( + ">>> from naming import Name\n" + ">>> class MyName(Name):\n" + "... config = dict(base=r'\\w+')\n" + "...\n" + ">>> n = MyName()\n" + "'{base}'" + ), + ( + "from naming import Name\n" + "\n" + "class MyName(Name):\n" + " config = dict(base=r'\\w+')\n" + "\n" + "n = MyName()\n" + ), + ), + ( + [ + ">>> class Foo:", + "... x = 1", + "...", + ">>> Foo()", + ], + MULTILINE_CLASS_EXPECTED, + ), + ("not a doctest", ""), + ([">>> ..."], "...\n"), + ], + ids=[ + "strips-output", + "example-generator", + "multiline-class", + "name-class", + "directive-lines", + "non-doctest", + "explicit-ellipsis", + ], +) +def test_doctest_to_replay_source(source, expected): + assert doctest_to_replay_source(source) == expected From eaf3015e2d1ed2019f46c39d08f57ead8b87bb2c Mon Sep 17 00:00:00 2001 From: Cursor Agent <cursoragent@cursor.com> Date: Thu, 2 Jul 2026 12:21:45 +0000 Subject: [PATCH 3/5] Phase 3: migrate scope and directive tests to doctree layer Move autodoc scope and py-repl directive tests into tests/doctree/ using sphinx-pytest instead of full HTML builds. Remove redundant tests covered by unit tests (replay body, multiline terminator, package doctree checks). Co-authored-by: chrizzftd <chrizzFTD@users.noreply.github.com> --- tests/conftest.py | 17 ---- tests/doctree/test_autodoc_scope.py | 64 +++++++------ tests/integration/test_autodoc_wiring.py | 70 ++++++++++++++ tests/integration/test_build_output.py | 80 ++++++++++++++++ tests/integration/test_include.py | 79 ++++++++++++++++ tests/integration/test_nested_pages.py | 42 +++++++++ tests/test_autodoc_doctest.py | 111 ----------------------- tests/test_basic.py | 69 -------------- tests/test_build_replay.py | 36 +------- tests/test_local_wheel.py | 33 +------ 10 files changed, 304 insertions(+), 297 deletions(-) create mode 100644 tests/integration/test_autodoc_wiring.py create mode 100644 tests/integration/test_build_output.py create mode 100644 tests/integration/test_include.py create mode 100644 tests/integration/test_nested_pages.py delete mode 100644 tests/test_autodoc_doctest.py delete mode 100644 tests/test_basic.py diff --git a/tests/conftest.py b/tests/conftest.py index 8f9f986..1e452c1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,5 @@ """Shared pytest fixtures.""" -from pathlib import Path - import pytest from support import ( @@ -14,21 +12,6 @@ ) -@pytest.fixture -def html_builder(tmp_path): - """Sphinx HTML builder with index and nested api/module pages.""" - srcdir = tmp_path / "docs" - srcdir.mkdir() - (srcdir / "conf.py").write_text("", encoding="utf-8") - (srcdir / "index.rst").write_text("Test\n====\n", encoding="utf-8") - (srcdir / "api").mkdir() - (srcdir / "api" / "module.rst").write_text("API\n===\n", encoding="utf-8") - outdir = tmp_path / "_build" - doctreedir = tmp_path / "_doctree" - app = build_sphinx(srcdir, outdir, doctreedir) - return app.builder - - @pytest.fixture def wheel_paths(): """Paths to the vendored test wheel fixture.""" diff --git a/tests/doctree/test_autodoc_scope.py b/tests/doctree/test_autodoc_scope.py index a2db0d7..b573146 100644 --- a/tests/doctree/test_autodoc_scope.py +++ b/tests/doctree/test_autodoc_scope.py @@ -4,6 +4,26 @@ from fixtures.sources import AUTODOC_SCOPE_RST, EXAMPLE_GENERATOR_SOURCE +def _write_autodoc_conf(srcdir, scope) -> None: + scope_literal = repr(scope) + (srcdir / "conf.py").write_text( + f""" +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parent)) +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx_pyrepl_web", +] +pyrepl_doctest_blocks = {scope_literal} +pyrepl_js = "pyrepl.js" +""".strip() + + "\n", + encoding="utf-8", + ) + + @pytest.fixture def autodoc_doctree(sphinx_doctree: CreateDoctree): """Configure sphinx_doctree with an autodoc module and doctest scope RST.""" @@ -11,31 +31,14 @@ def autodoc_doctree(sphinx_doctree: CreateDoctree): EXAMPLE_GENERATOR_SOURCE, encoding="utf-8", ) - sphinx_doctree.set_conf( - { - "extensions": [ - "sphinx.ext.autodoc", - "sphinx.ext.napoleon", - "sphinx_pyrepl_web", - ], - "pyrepl_doctest_blocks": "autodoc", - "pyrepl_js": "pyrepl.js", - } - ) + _write_autodoc_conf(sphinx_doctree.srcdir, "autodoc") sphinx_doctree.buildername = "html" return sphinx_doctree -def count_pyrepl_nodes(doctree) -> int: - """Return the number of py-repl raw HTML nodes in a doctree.""" - from docutils import nodes - - count = 0 - for node in doctree.traverse(nodes.raw): - text = node.astext() - if "<py-repl" in text: - count += 1 - return count +def count_pyrepl_nodes(result) -> int: + """Return the number of py-repl raw HTML nodes in a doctree result.""" + return result.pformat().count("<py-repl") def test_autodoc_doctest_becomes_pyrepl(autodoc_doctree): @@ -56,17 +59,12 @@ def test_autodoc_doctest_becomes_pyrepl(autodoc_doctree): ], ids=["autodoc-only", "all-doctests", "disabled"], ) -def test_doctest_scope(autodoc_doctree, scope, expected_count): - autodoc_doctree.set_conf( - { - "extensions": [ - "sphinx.ext.autodoc", - "sphinx.ext.napoleon", - "sphinx_pyrepl_web", - ], - "pyrepl_doctest_blocks": scope, - "pyrepl_js": "pyrepl.js", - } +def test_doctest_scope(sphinx_doctree, scope, expected_count): + (sphinx_doctree.srcdir / "repl_test_demo.py").write_text( + EXAMPLE_GENERATOR_SOURCE, + encoding="utf-8", ) - result = autodoc_doctree(AUTODOC_SCOPE_RST) + _write_autodoc_conf(sphinx_doctree.srcdir, scope) + sphinx_doctree.buildername = "html" + result = sphinx_doctree(AUTODOC_SCOPE_RST) assert count_pyrepl_nodes(result) == expected_count diff --git a/tests/integration/test_autodoc_wiring.py b/tests/integration/test_autodoc_wiring.py new file mode 100644 index 0000000..46d6201 --- /dev/null +++ b/tests/integration/test_autodoc_wiring.py @@ -0,0 +1,70 @@ +from helpers import load_bootstrap_files, pyrepl_tag +from support import ( + FIXTURES, + WHEEL_NAME, + WHEEL_PATH, + autodoc_conf_header, + build_sphinx, + copy_wheel_to, + wheel_conf_extra, +) + + +def test_autodoc_with_packages_writes_bootstrap_and_wheel(tmp_path): + srcdir = tmp_path / "docs" + copy_wheel_to(srcdir) + outdir = tmp_path / "_build" + doctreedir = tmp_path / "_doctree" + + (srcdir / "conf.py").write_text( + autodoc_conf_header( + sys_path=str(FIXTURES / "pyrepl_test_pkg"), + extra=wheel_conf_extra(), + ), + encoding="utf-8", + ) + (srcdir / "index.rst").write_text( + ".. autofunction:: pyrepl_test_pkg.demo.example_generator\n", + encoding="utf-8", + ) + + app = build_sphinx(srcdir, outdir, doctreedir) + + html = (outdir / "index.html").read_text(encoding="utf-8") + assert f'packages="{WHEEL_PATH}"' in html + assert 'replay-src="_static/pyrepl/index-1.py"' in html + tag = pyrepl_tag(html) + assert 'src="_static/pyrepl/index-1-bootstrap.py"' in tag + assert (outdir / "_static" / "wheels" / WHEEL_NAME).is_file() + assert (outdir / "_static" / "pyrepl" / "index-1.py").is_file() + assert (outdir / "_static" / "pyrepl" / "index-1-bootstrap.py").is_file() + + assert app.env.get_doctree("index").get("pyrepl") + assert list(load_bootstrap_files(app, "index")) == ["index-1-bootstrap.py"] + + +def test_autodoc_without_packages_is_replay_only(tmp_path): + from fixtures.sources import WIDGET_SOURCE + + pkg_dir = tmp_path / "installed_pkg" + pkg_dir.mkdir() + (pkg_dir / "__init__.py").write_text(WIDGET_SOURCE, encoding="utf-8") + + srcdir = tmp_path / "docs" + srcdir.mkdir() + outdir = tmp_path / "_build" + doctreedir = tmp_path / "_doctree" + + (srcdir / "conf.py").write_text( + autodoc_conf_header(sys_path=str(pkg_dir.parent)), + encoding="utf-8", + ) + (srcdir / "index.rst").write_text(".. autoclass:: installed_pkg.Widget\n", encoding="utf-8") + + build_sphinx(srcdir, outdir, doctreedir) + + html = (outdir / "index.html").read_text(encoding="utf-8") + assert 'replay-src="_static/pyrepl/index-1.py"' in html + tag = pyrepl_tag(html) + assert 'packages="' not in tag + assert ' src="' not in tag diff --git a/tests/integration/test_build_output.py b/tests/integration/test_build_output.py new file mode 100644 index 0000000..565dbab --- /dev/null +++ b/tests/integration/test_build_output.py @@ -0,0 +1,80 @@ +from helpers import assert_replay_artifacts +from support import WHEEL_NAME, WHEEL_PATH, build_sphinx + + +def test_build_writes_replay_script_from_metadata(sphinx_project): + srcdir, outdir, doctreedir = sphinx_project + app = build_sphinx(srcdir, outdir, doctreedir) + + replay_files = assert_replay_artifacts(app, outdir, "index") + script_name = next(iter(replay_files)) + script_path = outdir / "_static" / "pyrepl" / script_name + assert "x = 2 + 2" in script_path.read_text(encoding="utf-8") + + html = (outdir / "index.html").read_text(encoding="utf-8") + assert f'replay-src="_static/pyrepl/{script_name}"' in html + + +def test_build_writes_replay_script_with_parallel_read(sphinx_project): + srcdir, outdir, doctreedir = sphinx_project + build_sphinx(srcdir, outdir, doctreedir, parallel=2) + + script_path = outdir / "_static" / "pyrepl" / "index-1.py" + assert script_path.is_file(), f"missing replay script at {script_path}" + + +def test_local_wheel_copied_to_output_tree(wheel_project): + srcdir, outdir, doctreedir = wheel_project + (srcdir / "index.rst").write_text( + f""" +Example +======= + +.. py-repl:: + :packages: {WHEEL_PATH} + :no-header: +""", + encoding="utf-8", + ) + + build_sphinx(srcdir, outdir, doctreedir) + + wheel_out = outdir / "_static" / "wheels" / WHEEL_NAME + assert wheel_out.is_file(), f"missing wheel at {wheel_out}" + + html = (outdir / "index.html").read_text(encoding="utf-8") + assert f'packages="{WHEEL_PATH}"' in html + assert "pyrepl.js" in html + + +def test_local_wheel_with_src_and_replay_body(wheel_project): + srcdir, outdir, doctreedir = wheel_project + (srcdir / "index.rst").write_text( + f""" +Example +======= + +.. py-repl:: + :packages: {WHEEL_PATH} + :src: _static/bootstrap.py + :repl-title: test + :no-banner: + + >>> import pyrepl_test_pkg + >>> pyrepl_test_pkg.ping() +""", + encoding="utf-8", + ) + + app = build_sphinx(srcdir, outdir, doctreedir) + + assert (outdir / "_static" / "wheels" / WHEEL_NAME).is_file() + assert (outdir / "_static" / "bootstrap.py").is_file() + + replay_files = assert_replay_artifacts(app, outdir, "index", count=1) + script_name = next(iter(replay_files)) + + html = (outdir / "index.html").read_text(encoding="utf-8") + assert f'packages="{WHEEL_PATH}"' in html + assert 'src="_static/bootstrap.py"' in html + assert f'replay-src="_static/pyrepl/{script_name}"' in html diff --git a/tests/integration/test_include.py b/tests/integration/test_include.py new file mode 100644 index 0000000..7a7011b --- /dev/null +++ b/tests/integration/test_include.py @@ -0,0 +1,79 @@ +import pytest + +from helpers import assert_replay_artifacts +from support import build_sphinx + + +@pytest.fixture +def included_example_project(tmp_path): + """Project where example content is included into index.rst (like RTD docs).""" + srcdir = tmp_path / "docs" + srcdir.mkdir() + outdir = tmp_path / "_build" + doctreedir = tmp_path / "_doctree" + + (srcdir / "conf.py").write_text( + """ +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parent / "_static")) +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx_pyrepl_web", +] +master_doc = "index" +pyrepl_js = "pyrepl.js" +pyrepl_doctest_blocks = "autodoc" +exclude_patterns = ["example.rst"] +""", + encoding="utf-8", + ) + (srcdir / "_static").mkdir() + (srcdir / "_static" / "repl_include_demo.py").write_text( + ''' +def example_generator(n): + """Example. + + Examples: + >>> print(list(example_generator(2))) + [0, 1] + + """ + yield from range(n) +'''.strip() + + "\n", + encoding="utf-8", + ) + (srcdir / "example.rst").write_text( + """ +.. py-repl:: + :no-header: + + >>> 1 + 1 + +.. autofunction:: repl_include_demo.example_generator +""".strip() + + "\n", + encoding="utf-8", + ) + (srcdir / "index.rst").write_text( + ".. include:: example.rst\n", + encoding="utf-8", + ) + return srcdir, outdir, doctreedir + + +def test_included_example_writes_all_replay_scripts(included_example_project): + srcdir, outdir, doctreedir = included_example_project + app = build_sphinx(srcdir, outdir, doctreedir) + + replay_files = assert_replay_artifacts(app, outdir, "index", count=2) + + html = (outdir / "index.html").read_text(encoding="utf-8") + assert html.count("replay-src=") == 2 + assert 'src="_static/repl_include_demo.py"' not in html + + for script_name in replay_files: + script_path = outdir / "_static" / "pyrepl" / script_name + assert script_path.is_file(), f"missing replay script at {script_path}" diff --git a/tests/integration/test_nested_pages.py b/tests/integration/test_nested_pages.py new file mode 100644 index 0000000..d443abc --- /dev/null +++ b/tests/integration/test_nested_pages.py @@ -0,0 +1,42 @@ +from support import WHEEL_PATH, build_sphinx, copy_wheel_to, pyrepl_conf_header + + +def test_nested_page_emits_page_relative_static_paths(tmp_path): + srcdir = tmp_path / "docs" + copy_wheel_to(srcdir) + (srcdir / "api").mkdir() + outdir = tmp_path / "_build" + doctreedir = tmp_path / "_doctree" + + (srcdir / "conf.py").write_text( + pyrepl_conf_header(extra='html_static_path = ["_static"]\n'), + encoding="utf-8", + ) + (srcdir / "index.rst").write_text("Home\n====\n", encoding="utf-8") + (srcdir / "api" / "index.rst").write_text( + f""" +API +=== + +.. py-repl:: + :packages: {WHEEL_PATH} + :no-header: + + >>> 1 + 1 +""", + encoding="utf-8", + ) + + app = build_sphinx(srcdir, outdir, doctreedir) + + html = (outdir / "api" / "index.html").read_text(encoding="utf-8") + assert 'packages="../_static/wheels/' in html + assert 'packages="api/_static/' not in html + assert 'packages="/_static/' not in html + assert 'replay-src="../_static/pyrepl/api-index-1.py"' in html + + root_html = (outdir / "index.html").read_text(encoding="utf-8") + assert "py-repl" not in root_html + + replay_files = app.env.metadata["api/index"].get("pyrepl-replay-files") + assert replay_files is not None diff --git a/tests/test_autodoc_doctest.py b/tests/test_autodoc_doctest.py deleted file mode 100644 index 1396c93..0000000 --- a/tests/test_autodoc_doctest.py +++ /dev/null @@ -1,111 +0,0 @@ -import pytest - -from helpers import assert_replay_artifacts, load_replay_files -from support import autodoc_conf_header, build_sphinx - -EXAMPLE_GENERATOR_SOURCE = ''' -def example_generator(n): - """Generators have a ``Yields`` section instead of a ``Returns`` section. - - Examples: - Examples should be written in doctest format, and should illustrate how - to use the function. - - >>> print([i for i in example_generator(4)]) - [0, 1, 2, 3] - - """ - yield from range(n) -'''.strip() + "\n" - - -@pytest.fixture -def autodoc_project(tmp_path): - mod_dir = tmp_path / "pkg" - mod_dir.mkdir() - (mod_dir / "repl_test_demo.py").write_text(EXAMPLE_GENERATOR_SOURCE, encoding="utf-8") - - srcdir = tmp_path / "docs" - srcdir.mkdir() - outdir = tmp_path / "_build" - doctreedir = tmp_path / "_doctree" - - (srcdir / "conf.py").write_text( - autodoc_conf_header(sys_path=str(mod_dir)), - encoding="utf-8", - ) - (srcdir / "index.rst").write_text( - """ -API -=== - -.. autofunction:: repl_test_demo.example_generator - -Tutorial -======== - -Plain doctest (outside autodoc): - ->>> x = 1 ->>> x + 1 -2 -""".strip() - + "\n", - encoding="utf-8", - ) - return srcdir, outdir, doctreedir, mod_dir - - -def test_autodoc_doctest_becomes_pyrepl(autodoc_project): - srcdir, outdir, doctreedir, _ = autodoc_project - app = build_sphinx(srcdir, outdir, doctreedir) - - replay_files = assert_replay_artifacts(app, outdir, "index", count=1) - script_name = next(iter(replay_files)) - script = replay_files[script_name] - assert script == "print([i for i in example_generator(4)])\n" - assert "[0, 1, 2, 3]" not in script - - html = (outdir / "index.html").read_text(encoding="utf-8") - assert f'replay-src="_static/pyrepl/{script_name}"' in html - assert 'packages="' not in html - assert "no-header" in html - assert "no-banner" in html - - -def test_autodoc_scope_skips_plain_rst_doctest(autodoc_project): - srcdir, outdir, doctreedir, _ = autodoc_project - app = build_sphinx(srcdir, outdir, doctreedir) - - assert len(load_replay_files(app, "index")) == 1 - - html = (outdir / "index.html").read_text(encoding="utf-8") - assert html.count("replay-src=") == 1 - - -def test_all_scope_transforms_plain_rst_doctest(autodoc_project): - srcdir, outdir, doctreedir, _ = autodoc_project - conf = (srcdir / "conf.py").read_text(encoding="utf-8") - (srcdir / "conf.py").write_text( - conf + '\npyrepl_doctest_blocks = "all"\n', encoding="utf-8" - ) - app = build_sphinx(srcdir, outdir, doctreedir) - - assert len(load_replay_files(app, "index")) == 2 - - html = (outdir / "index.html").read_text(encoding="utf-8") - assert html.count("replay-src=") == 2 - - -def test_default_scope_leaves_doctest_static(autodoc_project): - srcdir, outdir, doctreedir, _ = autodoc_project - conf = (srcdir / "conf.py").read_text(encoding="utf-8") - (srcdir / "conf.py").write_text( - conf.replace('pyrepl_doctest_blocks = "autodoc"\n', ""), encoding="utf-8" - ) - app = build_sphinx(srcdir, outdir, doctreedir) - - assert load_replay_files(app, "index") == {} - - html = (outdir / "index.html").read_text(encoding="utf-8") - assert "replay-src=" not in html diff --git a/tests/test_basic.py b/tests/test_basic.py deleted file mode 100644 index 42f9df1..0000000 --- a/tests/test_basic.py +++ /dev/null @@ -1,69 +0,0 @@ -from sphinx_pytest.plugin import CreateDoctree - - -def test_basic(sphinx_doctree: CreateDoctree): - sphinx_doctree.set_conf({"extensions": ["sphinx_pyrepl_web"]}) - sphinx_doctree.buildername = "html" - result = sphinx_doctree( - """ -Test ----- - -.. py-repl:: - :theme: catppuccin-latte - :no-header: - -.. py-repl:: - - """ - ) - lines = [line.rstrip() for line in result.pformat().strip().splitlines()] - # Sphinx may serialize doctree bool attrs as "1" or "True" depending on version. - lines[0] = lines[0].replace('pyrepl="True"', 'pyrepl="1"') - assert lines == """ -<document pyrepl="1" source="<src>/index.rst"> - <section ids="test" names="test"> - <title> - Test - <raw format="html" xml:space="preserve"> - <py-repl theme="catppuccin-latte" no-header></py-repl> - <raw format="html" xml:space="preserve"> - <py-repl></py-repl> - """.strip().splitlines() - - -def test_replay_body(sphinx_doctree: CreateDoctree): - sphinx_doctree.set_conf({"extensions": ["sphinx_pyrepl_web"]}) - sphinx_doctree.buildername = "html" - result = sphinx_doctree( - """ -.. py-repl:: - :no-header: - - >>> x = 1 - >>> x + 1 - """ - ) - html = result.pformat() - assert 'replay-src="_static/pyrepl/index-1.py"' in html - - -def test_replay_file_flag(sphinx_doctree: CreateDoctree): - sphinx_doctree.set_conf( - { - "extensions": ["sphinx_pyrepl_web"], - "root_doc": "index", - } - ) - sphinx_doctree.buildername = "html" - (sphinx_doctree.srcdir / "demo.py").write_text("print('hi')\n", encoding="utf-8") - result = sphinx_doctree( - """ -.. py-repl:: - :src: demo.py - :replay: - """ - ) - html = result.pformat() - assert 'src="demo.py"' in html - assert "replay" in html diff --git a/tests/test_build_replay.py b/tests/test_build_replay.py index ab3fa66..f184daa 100644 --- a/tests/test_build_replay.py +++ b/tests/test_build_replay.py @@ -1,5 +1,5 @@ from helpers import assert_replay_artifacts -from support import build_sphinx, pyrepl_conf_header +from support import build_sphinx def test_build_writes_replay_script_from_metadata(sphinx_project): @@ -21,37 +21,3 @@ def test_build_writes_replay_script_with_parallel_read(sphinx_project): script_path = outdir / "_static" / "pyrepl" / "index-1.py" assert script_path.is_file(), f"missing replay script at {script_path}" - - -def test_build_omits_bare_doctest_terminator(tmp_path): - srcdir = tmp_path / "docs" - srcdir.mkdir() - outdir = tmp_path / "_build" - doctreedir = tmp_path / "_doctree" - (srcdir / "conf.py").write_text( - pyrepl_conf_header(), - encoding="utf-8", - ) - (srcdir / "index.rst").write_text( - """ -Example -======= - -.. py-repl:: - :no-header: - - >>> class Foo: - ... x = 1 - ... - >>> Foo() -""".strip() - + "\n", - encoding="utf-8", - ) - build_sphinx(srcdir, outdir, doctreedir) - - script_path = outdir / "_static" / "pyrepl" / "index-1.py" - script = script_path.read_text(encoding="utf-8") - assert "class Foo:" in script - assert "\n...\n" not in script - assert " x = 1\n\nFoo()" in script diff --git a/tests/test_local_wheel.py b/tests/test_local_wheel.py index 157f0c4..4617786 100644 --- a/tests/test_local_wheel.py +++ b/tests/test_local_wheel.py @@ -1,23 +1,7 @@ -from sphinx_pytest.plugin import CreateDoctree - -from helpers import assert_replay_artifacts, load_replay_files +from helpers import assert_replay_artifacts from support import WHEEL_NAME, WHEEL_PATH, build_sphinx -def test_local_wheel_packages_emitted_in_doctree(sphinx_doctree: CreateDoctree): - sphinx_doctree.set_conf({"extensions": ["sphinx_pyrepl_web"]}) - sphinx_doctree.buildername = "html" - result = sphinx_doctree( - f""" -.. py-repl:: - :packages: {WHEEL_PATH} - :no-header: -""" - ) - html = result.pformat() - assert f'packages="{WHEEL_PATH}"' in html - - def test_local_wheel_copied_to_output_tree(wheel_project): srcdir, outdir, doctreedir = wheel_project (srcdir / "index.rst").write_text( @@ -73,18 +57,3 @@ def test_local_wheel_with_src_and_replay_body(wheel_project): assert f'packages="{WHEEL_PATH}"' in html assert 'src="_static/bootstrap.py"' in html assert f'replay-src="_static/pyrepl/{script_name}"' in html - - -def test_comma_separated_local_wheel_and_pypi_package(sphinx_doctree: CreateDoctree): - sphinx_doctree.set_conf({"extensions": ["sphinx_pyrepl_web"]}) - sphinx_doctree.buildername = "html" - packages = f"numpy, {WHEEL_PATH}" - result = sphinx_doctree( - f""" -.. py-repl:: - :packages: {packages} - :no-header: -""" - ) - html = result.pformat() - assert f'packages="{packages}"' in html From 72f53df2ab887292adab810c8492603d69d40b5e Mon Sep 17 00:00:00 2001 From: Cursor Agent <cursoragent@cursor.com> Date: Thu, 2 Jul 2026 12:24:24 +0000 Subject: [PATCH 4/5] Phases 4-5: consolidate integration tests and fill coverage gaps Reorganize full-build tests into tests/integration/, remove redundant top-level integration files, and add targeted tests for directive flags, missing src errors, copy_asset_files edge cases, vendored JS copy, and startup file deduplication. Suite is now 47 tests with 100% coverage. Co-authored-by: chrizzftd <chrizzFTD@users.noreply.github.com> --- tests/doctree/test_pyrepl_directive.py | 55 ++++++++++ tests/integration/test_build_output.py | 45 ++++++++ tests/test_autodoc_bootstrap.py | 140 ------------------------- tests/test_autodoc_include.py | 79 -------------- tests/test_build_replay.py | 23 ---- tests/test_local_wheel.py | 59 ----------- tests/test_nested_static_paths.py | 42 -------- tests/unit/test_copy_assets.py | 39 +++++++ 8 files changed, 139 insertions(+), 343 deletions(-) delete mode 100644 tests/test_autodoc_bootstrap.py delete mode 100644 tests/test_autodoc_include.py delete mode 100644 tests/test_build_replay.py delete mode 100644 tests/test_local_wheel.py delete mode 100644 tests/test_nested_static_paths.py create mode 100644 tests/unit/test_copy_assets.py diff --git a/tests/doctree/test_pyrepl_directive.py b/tests/doctree/test_pyrepl_directive.py index 933fd06..45b0084 100644 --- a/tests/doctree/test_pyrepl_directive.py +++ b/tests/doctree/test_pyrepl_directive.py @@ -1,3 +1,4 @@ +import pytest from sphinx_pytest.plugin import CreateDoctree @@ -50,3 +51,57 @@ def test_replay_file_flag(sphinx_doctree: CreateDoctree): html = result.pformat() assert 'src="demo.py"' in html assert "replay" in html + + +@pytest.mark.parametrize( + "options,expected_fragments", + [ + (":no-buttons:\n :readonly:", ["no-buttons", "readonly"]), + (":repl-title: My REPL", ['repl-title="My REPL"']), + ], + ids=["flag-options", "repl-title"], +) +def test_pyrepl_directive_options( + sphinx_doctree: CreateDoctree, options, expected_fragments +): + sphinx_doctree.set_conf({"extensions": ["sphinx_pyrepl_web"], "root_doc": "index"}) + sphinx_doctree.buildername = "html" + (sphinx_doctree.srcdir / "demo.py").write_text("print('hi')\n", encoding="utf-8") + result = sphinx_doctree( + f""" +.. py-repl:: + {options} + + >>> 1 + 1 +""" + ) + html = result.pformat() + for fragment in expected_fragments: + assert fragment in html + + +def test_silent_src_omitted_without_body(sphinx_doctree: CreateDoctree): + sphinx_doctree.set_conf({"extensions": ["sphinx_pyrepl_web"], "root_doc": "index"}) + sphinx_doctree.buildername = "html" + (sphinx_doctree.srcdir / "demo.py").write_text("print('hi')\n", encoding="utf-8") + result = sphinx_doctree( + """ +.. py-repl:: + :silent: + :src: demo.py +""" + ) + html = result.pformat() + assert 'src="demo.py"' not in html + + +def test_missing_src_file_reports_error(sphinx_doctree: CreateDoctree): + sphinx_doctree.set_conf({"extensions": ["sphinx_pyrepl_web"]}) + sphinx_doctree.buildername = "html" + result = sphinx_doctree( + """ +.. py-repl:: + :src: missing.py +""" + ) + assert "Could not read file" in result.warnings diff --git a/tests/integration/test_build_output.py b/tests/integration/test_build_output.py index 565dbab..ff7d2e7 100644 --- a/tests/integration/test_build_output.py +++ b/tests/integration/test_build_output.py @@ -1,6 +1,8 @@ from helpers import assert_replay_artifacts from support import WHEEL_NAME, WHEEL_PATH, build_sphinx +from sphinx_pyrepl_web import PYREPL_DIR + def test_build_writes_replay_script_from_metadata(sphinx_project): srcdir, outdir, doctreedir = sphinx_project @@ -78,3 +80,46 @@ def test_local_wheel_with_src_and_replay_body(wheel_project): assert f'packages="{WHEEL_PATH}"' in html assert 'src="_static/bootstrap.py"' in html assert f'replay-src="_static/pyrepl/{script_name}"' in html + + +def test_vendored_pyrepl_assets_copied(sphinx_project): + srcdir, outdir, doctreedir = sphinx_project + build_sphinx(srcdir, outdir, doctreedir) + + copied = {path.name for path in PYREPL_DIR.iterdir() if path.is_file()} + assert copied + for name in copied: + assert (outdir / name).is_file(), f"missing vendored asset {name}" + + +def test_startup_src_deduplicated_on_build(tmp_path): + srcdir = tmp_path / "docs" + srcdir.mkdir() + outdir = tmp_path / "_build" + doctreedir = tmp_path / "_doctree" + (srcdir / "shared.py").write_text("print('shared')\n", encoding="utf-8") + (srcdir / "conf.py").write_text( + """ +extensions = ["sphinx_pyrepl_web"] +master_doc = "index" +pyrepl_js = "pyrepl.js" +""", + encoding="utf-8", + ) + (srcdir / "index.rst").write_text( + """ +Example +======= + +.. py-repl:: + :src: shared.py + +.. py-repl:: + :src: shared.py +""", + encoding="utf-8", + ) + + build_sphinx(srcdir, outdir, doctreedir) + assert (outdir / "shared.py").is_file() + assert (outdir / "shared.py").read_text(encoding="utf-8") == "print('shared')\n" diff --git a/tests/test_autodoc_bootstrap.py b/tests/test_autodoc_bootstrap.py deleted file mode 100644 index bc4098e..0000000 --- a/tests/test_autodoc_bootstrap.py +++ /dev/null @@ -1,140 +0,0 @@ -from helpers import load_bootstrap_files, load_replay_files, pyrepl_tag -from support import ( - FIXTURES, - WHEEL_NAME, - WHEEL_PATH, - autodoc_conf_header, - build_sphinx, - copy_wheel_to, - wheel_conf_extra, -) - - -def test_autodoc_packages_emits_configured_wheel(tmp_path): - srcdir = tmp_path / "docs" - copy_wheel_to(srcdir) - outdir = tmp_path / "_build" - doctreedir = tmp_path / "_doctree" - - (srcdir / "conf.py").write_text( - autodoc_conf_header( - sys_path=str(FIXTURES / "pyrepl_test_pkg"), - extra=wheel_conf_extra(), - ), - encoding="utf-8", - ) - (srcdir / "index.rst").write_text( - ".. autofunction:: pyrepl_test_pkg.demo.example_generator\n", - encoding="utf-8", - ) - - app = build_sphinx(srcdir, outdir, doctreedir) - - html = (outdir / "index.html").read_text(encoding="utf-8") - assert f'packages="{WHEEL_PATH}"' in html - assert 'replay-src="_static/pyrepl/index-1.py"' in html - tag = pyrepl_tag(html) - assert 'src="_static/pyrepl/index-1-bootstrap.py"' in tag - assert (outdir / "_static" / "wheels" / WHEEL_NAME).is_file() - - doctree = app.env.get_doctree("index") - assert doctree.get("pyrepl") - - replay_files = load_replay_files(app, "index") - assert list(replay_files) == ["index-1.py"] - assert replay_files["index-1.py"] == ( - "print([i for i in example_generator(4)])\n" - ) - - bootstrap_files = load_bootstrap_files(app, "index") - assert list(bootstrap_files) == ["index-1-bootstrap.py"] - assert bootstrap_files["index-1-bootstrap.py"] == ( - "from pyrepl_test_pkg.demo import example_generator\n" - ) - assert (outdir / "_static" / "pyrepl" / "index-1-bootstrap.py").is_file() - - -def test_autodoc_packages_for_out_of_tree_module(tmp_path): - pkg_dir = tmp_path / "installed_pkg" - pkg_dir.mkdir() - (pkg_dir / "__init__.py").write_text( - ''' -class Widget: - """A demo widget. - - Example: - >>> w = Widget() - >>> w.label - 'ready' - - """ - label = "ready" -'''.strip() - + "\n", - encoding="utf-8", - ) - - srcdir = tmp_path / "docs" - copy_wheel_to(srcdir) - outdir = tmp_path / "_build" - doctreedir = tmp_path / "_doctree" - - (srcdir / "conf.py").write_text( - autodoc_conf_header( - sys_path=str(pkg_dir.parent), - extra=wheel_conf_extra(), - ), - encoding="utf-8", - ) - (srcdir / "index.rst").write_text(".. autoclass:: installed_pkg.Widget\n", encoding="utf-8") - - app = build_sphinx(srcdir, outdir, doctreedir) - - html = (outdir / "index.html").read_text(encoding="utf-8") - assert f'packages="{WHEEL_PATH}"' in html - assert 'replay-src="_static/pyrepl/index-1.py"' in html - tag = pyrepl_tag(html) - assert 'src="_static/pyrepl/index-1-bootstrap.py"' in tag - - bootstrap_files = load_bootstrap_files(app, "index") - assert bootstrap_files["index-1-bootstrap.py"] == "from installed_pkg import Widget\n" - - -def test_autodoc_without_packages_is_replay_only(tmp_path): - pkg_dir = tmp_path / "installed_pkg" - pkg_dir.mkdir() - (pkg_dir / "__init__.py").write_text( - ''' -class Widget: - """A demo widget. - - Example: - >>> w = Widget() - >>> w.label - 'ready' - - """ - label = "ready" -'''.strip() - + "\n", - encoding="utf-8", - ) - - srcdir = tmp_path / "docs" - srcdir.mkdir() - outdir = tmp_path / "_build" - doctreedir = tmp_path / "_doctree" - - (srcdir / "conf.py").write_text( - autodoc_conf_header(sys_path=str(pkg_dir.parent)), - encoding="utf-8", - ) - (srcdir / "index.rst").write_text(".. autoclass:: installed_pkg.Widget\n", encoding="utf-8") - - build_sphinx(srcdir, outdir, doctreedir) - - html = (outdir / "index.html").read_text(encoding="utf-8") - assert 'replay-src="_static/pyrepl/index-1.py"' in html - tag = pyrepl_tag(html) - assert 'packages="' not in tag - assert ' src="' not in tag diff --git a/tests/test_autodoc_include.py b/tests/test_autodoc_include.py deleted file mode 100644 index 7a7011b..0000000 --- a/tests/test_autodoc_include.py +++ /dev/null @@ -1,79 +0,0 @@ -import pytest - -from helpers import assert_replay_artifacts -from support import build_sphinx - - -@pytest.fixture -def included_example_project(tmp_path): - """Project where example content is included into index.rst (like RTD docs).""" - srcdir = tmp_path / "docs" - srcdir.mkdir() - outdir = tmp_path / "_build" - doctreedir = tmp_path / "_doctree" - - (srcdir / "conf.py").write_text( - """ -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent / "_static")) -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.napoleon", - "sphinx_pyrepl_web", -] -master_doc = "index" -pyrepl_js = "pyrepl.js" -pyrepl_doctest_blocks = "autodoc" -exclude_patterns = ["example.rst"] -""", - encoding="utf-8", - ) - (srcdir / "_static").mkdir() - (srcdir / "_static" / "repl_include_demo.py").write_text( - ''' -def example_generator(n): - """Example. - - Examples: - >>> print(list(example_generator(2))) - [0, 1] - - """ - yield from range(n) -'''.strip() - + "\n", - encoding="utf-8", - ) - (srcdir / "example.rst").write_text( - """ -.. py-repl:: - :no-header: - - >>> 1 + 1 - -.. autofunction:: repl_include_demo.example_generator -""".strip() - + "\n", - encoding="utf-8", - ) - (srcdir / "index.rst").write_text( - ".. include:: example.rst\n", - encoding="utf-8", - ) - return srcdir, outdir, doctreedir - - -def test_included_example_writes_all_replay_scripts(included_example_project): - srcdir, outdir, doctreedir = included_example_project - app = build_sphinx(srcdir, outdir, doctreedir) - - replay_files = assert_replay_artifacts(app, outdir, "index", count=2) - - html = (outdir / "index.html").read_text(encoding="utf-8") - assert html.count("replay-src=") == 2 - assert 'src="_static/repl_include_demo.py"' not in html - - for script_name in replay_files: - script_path = outdir / "_static" / "pyrepl" / script_name - assert script_path.is_file(), f"missing replay script at {script_path}" diff --git a/tests/test_build_replay.py b/tests/test_build_replay.py deleted file mode 100644 index f184daa..0000000 --- a/tests/test_build_replay.py +++ /dev/null @@ -1,23 +0,0 @@ -from helpers import assert_replay_artifacts -from support import build_sphinx - - -def test_build_writes_replay_script_from_metadata(sphinx_project): - srcdir, outdir, doctreedir = sphinx_project - app = build_sphinx(srcdir, outdir, doctreedir) - - replay_files = assert_replay_artifacts(app, outdir, "index") - script_name = next(iter(replay_files)) - script_path = outdir / "_static" / "pyrepl" / script_name - assert "x = 2 + 2" in script_path.read_text(encoding="utf-8") - - html = (outdir / "index.html").read_text(encoding="utf-8") - assert f'replay-src="_static/pyrepl/{script_name}"' in html - - -def test_build_writes_replay_script_with_parallel_read(sphinx_project): - srcdir, outdir, doctreedir = sphinx_project - build_sphinx(srcdir, outdir, doctreedir, parallel=2) - - script_path = outdir / "_static" / "pyrepl" / "index-1.py" - assert script_path.is_file(), f"missing replay script at {script_path}" diff --git a/tests/test_local_wheel.py b/tests/test_local_wheel.py deleted file mode 100644 index 4617786..0000000 --- a/tests/test_local_wheel.py +++ /dev/null @@ -1,59 +0,0 @@ -from helpers import assert_replay_artifacts -from support import WHEEL_NAME, WHEEL_PATH, build_sphinx - - -def test_local_wheel_copied_to_output_tree(wheel_project): - srcdir, outdir, doctreedir = wheel_project - (srcdir / "index.rst").write_text( - f""" -Example -======= - -.. py-repl:: - :packages: {WHEEL_PATH} - :no-header: -""", - encoding="utf-8", - ) - - build_sphinx(srcdir, outdir, doctreedir) - - wheel_out = outdir / "_static" / "wheels" / WHEEL_NAME - assert wheel_out.is_file(), f"missing wheel at {wheel_out}" - - html = (outdir / "index.html").read_text(encoding="utf-8") - assert f'packages="{WHEEL_PATH}"' in html - assert "pyrepl.js" in html - - -def test_local_wheel_with_src_and_replay_body(wheel_project): - srcdir, outdir, doctreedir = wheel_project - (srcdir / "index.rst").write_text( - f""" -Example -======= - -.. py-repl:: - :packages: {WHEEL_PATH} - :src: _static/bootstrap.py - :repl-title: test - :no-banner: - - >>> import pyrepl_test_pkg - >>> pyrepl_test_pkg.ping() -""", - encoding="utf-8", - ) - - app = build_sphinx(srcdir, outdir, doctreedir) - - assert (outdir / "_static" / "wheels" / WHEEL_NAME).is_file() - assert (outdir / "_static" / "bootstrap.py").is_file() - - replay_files = assert_replay_artifacts(app, outdir, "index", count=1) - script_name = next(iter(replay_files)) - - html = (outdir / "index.html").read_text(encoding="utf-8") - assert f'packages="{WHEEL_PATH}"' in html - assert 'src="_static/bootstrap.py"' in html - assert f'replay-src="_static/pyrepl/{script_name}"' in html diff --git a/tests/test_nested_static_paths.py b/tests/test_nested_static_paths.py deleted file mode 100644 index d443abc..0000000 --- a/tests/test_nested_static_paths.py +++ /dev/null @@ -1,42 +0,0 @@ -from support import WHEEL_PATH, build_sphinx, copy_wheel_to, pyrepl_conf_header - - -def test_nested_page_emits_page_relative_static_paths(tmp_path): - srcdir = tmp_path / "docs" - copy_wheel_to(srcdir) - (srcdir / "api").mkdir() - outdir = tmp_path / "_build" - doctreedir = tmp_path / "_doctree" - - (srcdir / "conf.py").write_text( - pyrepl_conf_header(extra='html_static_path = ["_static"]\n'), - encoding="utf-8", - ) - (srcdir / "index.rst").write_text("Home\n====\n", encoding="utf-8") - (srcdir / "api" / "index.rst").write_text( - f""" -API -=== - -.. py-repl:: - :packages: {WHEEL_PATH} - :no-header: - - >>> 1 + 1 -""", - encoding="utf-8", - ) - - app = build_sphinx(srcdir, outdir, doctreedir) - - html = (outdir / "api" / "index.html").read_text(encoding="utf-8") - assert 'packages="../_static/wheels/' in html - assert 'packages="api/_static/' not in html - assert 'packages="/_static/' not in html - assert 'replay-src="../_static/pyrepl/api-index-1.py"' in html - - root_html = (outdir / "index.html").read_text(encoding="utf-8") - assert "py-repl" not in root_html - - replay_files = app.env.metadata["api/index"].get("pyrepl-replay-files") - assert replay_files is not None diff --git a/tests/unit/test_copy_assets.py b/tests/unit/test_copy_assets.py new file mode 100644 index 0000000..99c7756 --- /dev/null +++ b/tests/unit/test_copy_assets.py @@ -0,0 +1,39 @@ +import json +from unittest.mock import MagicMock + +from docutils import nodes +from docutils.utils import new_document + +from sphinx_pyrepl_web import REPLAY_FILES_KEY, copy_asset_files, transform_doctest_blocks + + +def test_copy_asset_files_skips_non_html_builder(): + app = MagicMock() + app.builder.format = "latex" + copy_asset_files(app, None) + app.builder.outdir.__truediv__.assert_not_called() + + +def test_copy_asset_files_skips_empty_replay_metadata(tmp_path): + app = MagicMock() + app.builder.format = "html" + app.builder.outdir = str(tmp_path / "out") + app.env.metadata = {"index": {REPLAY_FILES_KEY: json.dumps({})}} + copy_asset_files(app, None) + assert not (tmp_path / "out" / "_static" / "pyrepl").exists() + + +def test_transform_skips_empty_converted_doctest(): + app = MagicMock() + app.config.pyrepl_doctest_blocks = "all" + app.env.docname = "index" + app.env.metadata = {"index": {}} + app.builder.format = "html" + + doctree = new_document("<test>") + block = nodes.doctest_block("", "") + doctree += block + + transform_doctest_blocks(app, doctree) + assert len(list(doctree.findall(nodes.doctest_block))) == 1 + assert len(list(doctree.findall(nodes.raw))) == 0 From 10153a84b4641f860eb276311dba3f590b4316dc Mon Sep 17 00:00:00 2001 From: Cursor Agent <cursoragent@cursor.com> Date: Thu, 2 Jul 2026 12:38:33 +0000 Subject: [PATCH 5/5] Address review: package imports, durable copy_assets test, remove dead fixture - Make tests/ a proper package with tests.* imports - Move sys.path setup into conftest.py only - Remove unused wheel_paths fixture - Strengthen non-HTML copy_asset_files test to assert no output files Co-authored-by: chrizzftd <chrizzFTD@users.noreply.github.com> --- tests/__init__.py | 1 + tests/conftest.py | 20 +++++++------------- tests/doctree/test_autodoc_scope.py | 2 +- tests/fixtures/__init__.py | 1 + tests/integration/test_autodoc_wiring.py | 6 +++--- tests/integration/test_build_output.py | 4 ++-- tests/integration/test_include.py | 4 ++-- tests/integration/test_nested_pages.py | 2 +- tests/support.py | 3 --- tests/unit/test_asset_href.py | 2 +- tests/unit/test_config.py | 2 +- tests/unit/test_copy_assets.py | 22 +++++++++++++++++++--- 12 files changed, 39 insertions(+), 30 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/fixtures/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..9a1763a --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test package for sphinx-pyrepl-web.""" diff --git a/tests/conftest.py b/tests/conftest.py index 1e452c1..fa87a25 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,21 +1,15 @@ """Shared pytest fixtures.""" -import pytest +import sys +from pathlib import Path -from support import ( - FIXTURES, - WHEEL_NAME, - WHEEL_PATH, - build_sphinx, - copy_wheel_to, - pyrepl_conf_header, -) +import pytest +from tests.support import build_sphinx, copy_wheel_to, pyrepl_conf_header -@pytest.fixture -def wheel_paths(): - """Paths to the vendored test wheel fixture.""" - return FIXTURES / "wheels", WHEEL_NAME, WHEEL_PATH +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) @pytest.fixture diff --git a/tests/doctree/test_autodoc_scope.py b/tests/doctree/test_autodoc_scope.py index b573146..fdc7821 100644 --- a/tests/doctree/test_autodoc_scope.py +++ b/tests/doctree/test_autodoc_scope.py @@ -1,7 +1,7 @@ import pytest from sphinx_pytest.plugin import CreateDoctree -from fixtures.sources import AUTODOC_SCOPE_RST, EXAMPLE_GENERATOR_SOURCE +from tests.fixtures.sources import AUTODOC_SCOPE_RST, EXAMPLE_GENERATOR_SOURCE def _write_autodoc_conf(srcdir, scope) -> None: diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 0000000..315591d --- /dev/null +++ b/tests/fixtures/__init__.py @@ -0,0 +1 @@ +"""Shared test fixtures and sample sources.""" diff --git a/tests/integration/test_autodoc_wiring.py b/tests/integration/test_autodoc_wiring.py index 46d6201..d955d71 100644 --- a/tests/integration/test_autodoc_wiring.py +++ b/tests/integration/test_autodoc_wiring.py @@ -1,5 +1,5 @@ -from helpers import load_bootstrap_files, pyrepl_tag -from support import ( +from tests.helpers import load_bootstrap_files, pyrepl_tag +from tests.support import ( FIXTURES, WHEEL_NAME, WHEEL_PATH, @@ -44,7 +44,7 @@ def test_autodoc_with_packages_writes_bootstrap_and_wheel(tmp_path): def test_autodoc_without_packages_is_replay_only(tmp_path): - from fixtures.sources import WIDGET_SOURCE + from tests.fixtures.sources import WIDGET_SOURCE pkg_dir = tmp_path / "installed_pkg" pkg_dir.mkdir() diff --git a/tests/integration/test_build_output.py b/tests/integration/test_build_output.py index ff7d2e7..09b1713 100644 --- a/tests/integration/test_build_output.py +++ b/tests/integration/test_build_output.py @@ -1,5 +1,5 @@ -from helpers import assert_replay_artifacts -from support import WHEEL_NAME, WHEEL_PATH, build_sphinx +from tests.helpers import assert_replay_artifacts +from tests.support import WHEEL_NAME, WHEEL_PATH, build_sphinx from sphinx_pyrepl_web import PYREPL_DIR diff --git a/tests/integration/test_include.py b/tests/integration/test_include.py index 7a7011b..0bc3741 100644 --- a/tests/integration/test_include.py +++ b/tests/integration/test_include.py @@ -1,7 +1,7 @@ import pytest -from helpers import assert_replay_artifacts -from support import build_sphinx +from tests.helpers import assert_replay_artifacts +from tests.support import build_sphinx @pytest.fixture diff --git a/tests/integration/test_nested_pages.py b/tests/integration/test_nested_pages.py index d443abc..4c6c955 100644 --- a/tests/integration/test_nested_pages.py +++ b/tests/integration/test_nested_pages.py @@ -1,4 +1,4 @@ -from support import WHEEL_PATH, build_sphinx, copy_wheel_to, pyrepl_conf_header +from tests.support import WHEEL_PATH, build_sphinx, copy_wheel_to, pyrepl_conf_header def test_nested_page_emits_page_relative_static_paths(tmp_path): diff --git a/tests/support.py b/tests/support.py index 53c5b5e..7742620 100644 --- a/tests/support.py +++ b/tests/support.py @@ -1,7 +1,6 @@ """Shared constants and Sphinx project setup helpers.""" import shutil -import sys from pathlib import Path from sphinx.application import Sphinx @@ -11,8 +10,6 @@ WHEEL_NAME = "pyrepl_test_pkg-1.0.0-py3-none-any.whl" WHEEL_PATH = f"_static/wheels/{WHEEL_NAME}" -sys.path.insert(0, str(ROOT)) - def build_sphinx( srcdir: Path, diff --git a/tests/unit/test_asset_href.py b/tests/unit/test_asset_href.py index afb9599..66d39f6 100644 --- a/tests/unit/test_asset_href.py +++ b/tests/unit/test_asset_href.py @@ -2,7 +2,7 @@ import pytest -from helpers import mock_html_builder +from tests.helpers import mock_html_builder from sphinx_pyrepl_web import _asset_href, _asset_href_packages diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index f0b9d39..de0668f 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -1,7 +1,7 @@ import pytest from unittest.mock import MagicMock -from support import WHEEL_PATH +from tests.support import WHEEL_PATH from sphinx_pyrepl_web import _autodoc_packages diff --git a/tests/unit/test_copy_assets.py b/tests/unit/test_copy_assets.py index 99c7756..78c182e 100644 --- a/tests/unit/test_copy_assets.py +++ b/tests/unit/test_copy_assets.py @@ -4,14 +4,30 @@ from docutils import nodes from docutils.utils import new_document -from sphinx_pyrepl_web import REPLAY_FILES_KEY, copy_asset_files, transform_doctest_blocks +from sphinx_pyrepl_web import PYREPL_DIR, REPLAY_FILES_KEY, copy_asset_files, transform_doctest_blocks -def test_copy_asset_files_skips_non_html_builder(): +def test_copy_asset_files_skips_non_html_builder(tmp_path): + outdir = tmp_path / "out" + outdir.mkdir() + app = MagicMock() app.builder.format = "latex" + app.builder.outdir = str(outdir) + app.env.metadata = { + "index": { + REPLAY_FILES_KEY: json.dumps({"index-1.py": "print('hi')\n"}), + } + } + copy_asset_files(app, None) - app.builder.outdir.__truediv__.assert_not_called() + + assert not any(outdir.iterdir()) + assert not (outdir / "pyrepl.js").exists() + assert not (outdir / "_static").exists() + for asset in PYREPL_DIR.iterdir(): + if asset.is_file(): + assert not (outdir / asset.name).exists() def test_copy_asset_files_skips_empty_replay_metadata(tmp_path):