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
new file mode 100644
index 0000000..fa87a25
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,58 @@
+"""Shared pytest fixtures."""
+
+import sys
+from pathlib import Path
+
+import pytest
+
+from tests.support import build_sphinx, copy_wheel_to, pyrepl_conf_header
+
+ROOT = Path(__file__).resolve().parents[1]
+if str(ROOT) not in sys.path:
+ sys.path.insert(0, str(ROOT))
+
+
+@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/doctree/test_autodoc_scope.py b/tests/doctree/test_autodoc_scope.py
new file mode 100644
index 0000000..fdc7821
--- /dev/null
+++ b/tests/doctree/test_autodoc_scope.py
@@ -0,0 +1,70 @@
+import pytest
+from sphinx_pytest.plugin import CreateDoctree
+
+from tests.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."""
+ (sphinx_doctree.srcdir / "repl_test_demo.py").write_text(
+ EXAMPLE_GENERATOR_SOURCE,
+ encoding="utf-8",
+ )
+ _write_autodoc_conf(sphinx_doctree.srcdir, "autodoc")
+ sphinx_doctree.buildername = "html"
+ return sphinx_doctree
+
+
+def count_pyrepl_nodes(result) -> int:
+ """Return the number of py-repl raw HTML nodes in a doctree result."""
+ return result.pformat().count("
@@ -32,22 +32,6 @@ def test_basic(sphinx_doctree: CreateDoctree):
""".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(
{
@@ -67,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/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/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/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/integration/test_autodoc_wiring.py b/tests/integration/test_autodoc_wiring.py
new file mode 100644
index 0000000..d955d71
--- /dev/null
+++ b/tests/integration/test_autodoc_wiring.py
@@ -0,0 +1,70 @@
+from tests.helpers import load_bootstrap_files, pyrepl_tag
+from tests.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 tests.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..09b1713
--- /dev/null
+++ b/tests/integration/test_build_output.py
@@ -0,0 +1,125 @@
+from tests.helpers import assert_replay_artifacts
+from tests.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
+ 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
+
+
+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_include.py b/tests/integration/test_include.py
similarity index 70%
rename from tests/test_autodoc_include.py
rename to tests/integration/test_include.py
index 500229c..0bc3741 100644
--- a/tests/test_autodoc_include.py
+++ b/tests/integration/test_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 tests.helpers import assert_replay_artifacts
+from tests.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/integration/test_nested_pages.py b/tests/integration/test_nested_pages.py
new file mode 100644
index 0000000..4c6c955
--- /dev/null
+++ b/tests/integration/test_nested_pages.py
@@ -0,0 +1,42 @@
+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):
+ 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/support.py b/tests/support.py
new file mode 100644
index 0000000..7742620
--- /dev/null
+++ b/tests/support.py
@@ -0,0 +1,77 @@
+"""Shared constants and Sphinx project setup helpers."""
+
+import shutil
+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}"
+
+
+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
deleted file mode 100644
index fca2fb0..0000000
--- a/tests/test_asset_href.py
+++ /dev/null
@@ -1,98 +0,0 @@
-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 (
- _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
deleted file mode 100644
index c291a0a..0000000
--- a/tests/test_autodoc_bootstrap.py
+++ /dev/null
@@ -1,211 +0,0 @@
-import json
-import shutil
-import sys
-from pathlib import Path
-from unittest.mock import MagicMock
-
-from sphinx.application import Sphinx
-
-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"
- 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()}
-""",
- 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
- pyrepl_tag = html[html.index("", html.index("")]
- assert 'src="_static/pyrepl/index-1-bootstrap.py"' in pyrepl_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", "{}")
- )
- 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", "{}")
- )
- 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",
- )
-
- 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"
- 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()}
-""",
- 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
- pyrepl_tag = html[html.index("", html.index("")]
- assert 'src="_static/pyrepl/index-1-bootstrap.py"' in pyrepl_tag
-
- bootstrap_files = json.loads(
- app.env.metadata["index"].get("pyrepl-bootstrap-files", "{}")
- )
- 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(
- 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"
-""",
- 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
- pyrepl_tag = html[html.index("", html.index("")]
- assert 'packages="' not in pyrepl_tag
- assert ' src="' not in pyrepl_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_autodoc_doctest.py b/tests/test_autodoc_doctest.py
deleted file mode 100644
index 1433396..0000000
--- a/tests/test_autodoc_doctest.py
+++ /dev/null
@@ -1,158 +0,0 @@
-import json
-from pathlib import Path
-
-import pytest
-from sphinx.application import Sphinx
-
-ROOT = Path(__file__).resolve().parents[1]
-
-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(
- 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"
-""",
- 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 _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)
-
- replay_files = json.loads(
- app.env.metadata["index"].get("pyrepl-replay-files", "{}")
- )
- assert len(replay_files) == 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
-
- 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)
-
- replay_files = json.loads(
- app.env.metadata["index"].get("pyrepl-replay-files", "{}")
- )
- assert len(replay_files) == 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)
-
- replay_files = json.loads(
- app.env.metadata["index"].get("pyrepl-replay-files", "{}")
- )
- assert len(replay_files) == 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)
-
- replay_files = json.loads(
- app.env.metadata["index"].get("pyrepl-replay-files", "{}")
- )
- assert replay_files == {}
-
- html = (outdir / "index.html").read_text(encoding="utf-8")
- assert "replay-src=" not in html
diff --git a/tests/test_build_replay.py b/tests/test_build_replay.py
deleted file mode 100644
index 622bb94..0000000
--- a/tests/test_build_replay.py
+++ /dev/null
@@ -1,134 +0,0 @@
-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
-
-
-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()
-
- replay_files = json.loads(app.env.metadata["index"].get("pyrepl-replay-files", "{}"))
- assert replay_files
- 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")
- 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
- 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()
-
- 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(
- "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:
-
- >>> class Foo:
- ... x = 1
- ...
- >>> Foo()
-""".strip()
- + "\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()
-
- 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_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"
- 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/test_local_wheel.py b/tests/test_local_wheel.py
deleted file mode 100644
index 8cb19f4..0000000
--- a/tests/test_local_wheel.py
+++ /dev/null
@@ -1,148 +0,0 @@
-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
-
-
-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(
- 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 = json.loads(
- app.env.metadata["index"].get("pyrepl-replay-files", "{}")
- )
- assert len(replay_files) == 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
- 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
diff --git a/tests/test_nested_static_paths.py b/tests/test_nested_static_paths.py
deleted file mode 100644
index 2acc8cc..0000000
--- a/tests/test_nested_static_paths.py
+++ /dev/null
@@ -1,77 +0,0 @@
-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
-
-
-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)
- (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"]
-""",
- 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_asset_href.py b/tests/unit/test_asset_href.py
new file mode 100644
index 0000000..66d39f6
--- /dev/null
+++ b/tests/unit/test_asset_href.py
@@ -0,0 +1,53 @@
+from unittest.mock import MagicMock
+
+import pytest
+
+from tests.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..de0668f
--- /dev/null
+++ b/tests/unit/test_config.py
@@ -0,0 +1,20 @@
+import pytest
+from unittest.mock import MagicMock
+
+from tests.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_copy_assets.py b/tests/unit/test_copy_assets.py
new file mode 100644
index 0000000..78c182e
--- /dev/null
+++ b/tests/unit/test_copy_assets.py
@@ -0,0 +1,55 @@
+import json
+from unittest.mock import MagicMock
+
+from docutils import nodes
+from docutils.utils import new_document
+
+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(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)
+
+ 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):
+ 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("")
+ 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
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",
+ 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