diff --git a/README.md b/README.md
index f281a66..c9353e7 100644
--- a/README.md
+++ b/README.md
@@ -68,7 +68,7 @@ Optional Sphinx config:
```python
pyrepl_js = "../pyrepl.js" # default; path to the pyrepl-web loader script
pyrepl_doctest_blocks = False # default; see Docstring conversion below
-pyrepl_autodoc_bootstrap = True # default; silent :src: bootstrap for autodoc REPLs
+pyrepl_autodoc_packages = None # optional; wheel path or PyPI name for autodoc REPLs
```
### Docstring conversion
@@ -82,6 +82,7 @@ extensions = [
"sphinx_pyrepl_web",
]
pyrepl_doctest_blocks = "autodoc"
+pyrepl_autodoc_packages = "_static/wheels/my_package-1.0.0-py3-none-any.whl"
```
| | `pyrepl_doctest_blocks` options |
@@ -91,10 +92,24 @@ pyrepl_doctest_blocks = "autodoc"
| `"all"` | Transform every doctest block found |
-| | `pyrepl_autodoc_bootstrap` options |
-|------------------|------------------------------------------------------------------------------|
-| `True` (default) | Bootstrap REPL: in-tree modules via silent `:src:` only |
-| `False` | Replay doctest input only; documented names are not pre-defined |
+| | `pyrepl_autodoc_packages` options |
+|-------------------------|------------------------------------------------------------------|
+| unset / `None` / `""` | Replay doctest input only (no wheel install or auto-import) |
+| wheel path or PyPI name | Install the package and import the documented object before replay (comma-separated) |
+
+Autodoc integration assumes a single documented package. The wheel (or PyPI
+name) is installed in the browser REPL and the documented name is imported
+automatically so doctest examples can use unqualified names. Autodoc still
+imports the package on the host at build time.
+
+To build this project's docs locally:
+
+```bash
+pip install -e ".[docs]"
+```
+
+The `[docs]` extra installs doc build dependencies plus the `pyrepl_test_pkg`
+fixture used in the examples.
### Local Pyodide wheels
diff --git a/docs/_static/wheels/pyrepl_test_pkg-1.0.0-py3-none-any.whl b/docs/_static/wheels/pyrepl_test_pkg-1.0.0-py3-none-any.whl
index 527687e..d8a2685 100644
Binary files a/docs/_static/wheels/pyrepl_test_pkg-1.0.0-py3-none-any.whl and b/docs/_static/wheels/pyrepl_test_pkg-1.0.0-py3-none-any.whl differ
diff --git a/docs/conf.py b/docs/conf.py
index f1c65f0..262ee14 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,11 +1,7 @@
from datetime import date
-import sys
-from pathlib import Path
from sphinx_pyrepl_web import __version__
-sys.path.insert(0, str(Path(__file__).parent / "_static"))
-
project = "sphinx-pyrepl-web"
version = __version__
author = "Christian López Barrón"
@@ -18,6 +14,7 @@
"sphinx_pyrepl_web",
]
pyrepl_doctest_blocks = "autodoc"
+pyrepl_autodoc_packages = "_static/wheels/pyrepl_test_pkg-1.0.0-py3-none-any.whl"
html_static_path = ["_static"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
diff --git a/docs/example.rst b/docs/example.rst
index 4c1ac71..77573bc 100644
--- a/docs/example.rst
+++ b/docs/example.rst
@@ -123,31 +123,40 @@ Rendered result:
Autodoc
-------
-The documented module's source is loaded in advance before replay, so
-module members are available in the REPL namespace when the module lives
-under the Sphinx source tree (silent ``:src:``).
+When ``pyrepl_doctest_blocks = "autodoc"``, doctest examples in documented
+APIs become interactive REPLs. Set ``pyrepl_autodoc_packages`` to install the
+documented package from a Pyodide-compatible wheel (or PyPI name) and
+automatically import the documented object before replay:
+
+.. code-block:: python
+
+ # conf.py
+ html_static_path = ["_static"]
+ pyrepl_autodoc_packages = "_static/wheels/pyrepl_test_pkg-1.0.0-py3-none-any.whl"
+
+Autodoc still imports the package on the host at build time (for example via
+``pip install -e ".[docs]"`` in this repository).
Source module:
-.. literalinclude:: _static/autodoc_demo.py
+.. literalinclude:: ../../tests/fixtures/pyrepl_test_pkg/pyrepl_test_pkg/demo.py
:language: python
RST content:
.. code-block:: rst
- .. autofunction:: autodoc_demo.example_generator
+ .. autofunction:: pyrepl_test_pkg.demo.example_generator
Rendered result:
-.. autofunction:: autodoc_demo.example_generator
+.. autofunction:: pyrepl_test_pkg.demo.example_generator
Local Pyodide wheels
--------------------
-Preload a Pyodide-compatible wheel from ``_static/wheels/`` via ``:packages:``.
-Combine with ``:src:`` for optional post-install bootstrap and a doctest replay
-body:
+The same wheel can be referenced manually from ``.. py-repl::`` when you want
+a standalone REPL without autodoc:
.. code-block:: rst
diff --git a/pyproject.toml b/pyproject.toml
index 17db7c4..1f32c50 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,6 +37,7 @@ test = [
]
docs = [
"myst-parser",
+ "pyrepl_test_pkg @ file:./tests/fixtures/pyrepl_test_pkg",
]
[tool.pytest.ini_options]
diff --git a/scripts/build_test_pkg_wheel.py b/scripts/build_test_pkg_wheel.py
new file mode 100644
index 0000000..00bd9f2
--- /dev/null
+++ b/scripts/build_test_pkg_wheel.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+"""Build pyrepl_test_pkg wheel and copy it into docs and test fixture paths."""
+
+from __future__ import annotations
+
+import argparse
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+PKG_DIR = ROOT / "tests" / "fixtures" / "pyrepl_test_pkg"
+WHEEL_NAME = "pyrepl_test_pkg-1.0.0-py3-none-any.whl"
+DEST_DIRS = (
+ ROOT / "tests" / "fixtures" / "wheels",
+ ROOT / "docs" / "_static" / "wheels",
+)
+
+
+def run(cmd: list[str], *, cwd: Path) -> None:
+ print(f"+ {' '.join(cmd)}")
+ subprocess.run(cmd, cwd=cwd, check=True)
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "--keep-dist",
+ action="store_true",
+ help="Leave the build dist/ directory in the fixture package tree",
+ )
+ args = parser.parse_args()
+
+ dist_dir = PKG_DIR / "dist"
+ if dist_dir.exists():
+ shutil.rmtree(dist_dir)
+
+ run([sys.executable, "-m", "pip", "wheel", ".", "-w", "dist"], cwd=PKG_DIR)
+
+ built = dist_dir / WHEEL_NAME
+ if not built.is_file():
+ wheels = sorted(dist_dir.glob("*.whl"))
+ if len(wheels) != 1:
+ sys.exit(f"expected one wheel in {dist_dir}, found: {wheels!r}")
+ built = wheels[0]
+
+ for dest_dir in DEST_DIRS:
+ dest_dir.mkdir(parents=True, exist_ok=True)
+ dest = dest_dir / WHEEL_NAME
+ shutil.copy2(built, dest)
+ print(f"copied {built.name} -> {dest}")
+
+ if not args.keep_dist:
+ shutil.rmtree(dist_dir)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/sphinx_pyrepl_web/__init__.py b/sphinx_pyrepl_web/__init__.py
index 26af36f..881a4d7 100644
--- a/sphinx_pyrepl_web/__init__.py
+++ b/sphinx_pyrepl_web/__init__.py
@@ -2,12 +2,9 @@
__version__ = "0.2.0"
-import importlib
-import inspect
import json
from doctest import DocTestParser
from pathlib import Path
-import sys
from docutils import nodes
from docutils.parsers.rst import directives
@@ -20,6 +17,7 @@
PYREPL_DIR = Path(__file__).parent / "pyrepl"
STARTUP_FILES_KEY = "pyrepl-startup-files"
REPLAY_FILES_KEY = "pyrepl-replay-files"
+BOOTSTRAP_FILES_KEY = "pyrepl-bootstrap-files"
_DOCTEST_PARSER = DocTestParser()
logger = logging.getLogger(__name__)
@@ -28,7 +26,7 @@ def setup(app: Sphinx):
"""Setup the extension."""
app.add_config_value("pyrepl_js", "../pyrepl.js", "env")
app.add_config_value("pyrepl_doctest_blocks", False, "env")
- app.add_config_value("pyrepl_autodoc_bootstrap", True, "env")
+ app.add_config_value("pyrepl_autodoc_packages", None, "env")
app.add_directive("py-repl", PyRepl)
app.connect("doctree-read", doctree_read)
app.connect("doctree-read", transform_doctest_blocks)
@@ -59,8 +57,8 @@ def register_autodoc_repl(
env,
docname: str,
replay_text: str,
-) -> str:
- """Record a replay script in env metadata and return its replay-src path."""
+) -> tuple[str, str]:
+ """Record a replay script in env metadata and return (replay-src path, name)."""
replay_files = json.loads(
env.metadata[docname].setdefault(REPLAY_FILES_KEY, "{}")
)
@@ -68,27 +66,48 @@ def register_autodoc_repl(
replay_name = f"{docname.replace('/', '-')}-{counter}.py"
replay_files[replay_name] = replay_text
env.metadata[docname][REPLAY_FILES_KEY] = json.dumps(replay_files)
- return f"_static/pyrepl/{replay_name}"
+ return f"_static/pyrepl/{replay_name}", replay_name
-def register_startup_file(env, docname: str, path: Path) -> str:
- """Track a startup script under srcdir for copying into HTML output."""
- env.note_dependency(path)
- rel_src = path.relative_to(Path(env.srcdir)).as_posix()
- startup_files = json.loads(
- env.metadata[docname].setdefault(STARTUP_FILES_KEY, "[]")
+def register_autodoc_bootstrap(
+ env,
+ docname: str,
+ bootstrap_text: str,
+ replay_name: str,
+) -> str:
+ """Record a silent bootstrap script paired with a replay file."""
+ bootstrap_files = json.loads(
+ env.metadata[docname].setdefault(BOOTSTRAP_FILES_KEY, "{}")
)
- abs_path = str(path.resolve())
- if abs_path not in startup_files:
- startup_files.append(abs_path)
- env.metadata[docname][STARTUP_FILES_KEY] = json.dumps(startup_files)
- return rel_src
+ bootstrap_name = replay_name.replace(".py", "-bootstrap.py")
+ bootstrap_files[bootstrap_name] = bootstrap_text
+ env.metadata[docname][BOOTSTRAP_FILES_KEY] = json.dumps(bootstrap_files)
+ return f"_static/pyrepl/{bootstrap_name}"
+
+
+def autodoc_bootstrap_source(
+ module: str | None,
+ fullname: str | None,
+ objtype: str | None,
+) -> str | None:
+ """Return a silent import script for the documented autodoc object."""
+ if not module:
+ return None
+
+ if objtype == "module" or not fullname:
+ return f"import {module}\n"
+
+ if "." in fullname:
+ root = fullname.split(".", 1)[0]
+ return f"from {module} import {root}\n"
+
+ return f"from {module} import {fullname}\n"
def make_pyrepl_raw(
replay_src: str,
- src: str | None = None,
packages: str | None = None,
+ src: str | None = None,
) -> nodes.raw:
"""Build a raw HTML node for an autodoc doctest replay widget."""
attrs = ["no-header", "no-banner", f'replay-src="{replay_src}"']
@@ -110,51 +129,9 @@ def _find_autodoc_desc(node: nodes.Node) -> addnodes.desc | None:
return None
-def _resolve_autodoc_bootstrap(
- app: Sphinx, env, docname: str, desc: addnodes.desc
-) -> tuple[str | None, str | None]:
- """Return (startup src path, packages) for autodoc REPLs.
-
- Only modules whose source lives under the Sphinx source directory are
- bootstrapped via silent ``:src:``. Installed or out-of-tree modules are
- left without bootstrap; use explicit ``:packages:`` wheel paths instead.
- """
- if not app.config.pyrepl_autodoc_bootstrap:
- return None, None
-
- sig = desc.next_node(addnodes.desc_signature)
- if sig is None:
- return None, None
-
- module_name = sig.get("module")
- fullname = sig.get("fullname")
- if not module_name:
- return None, None
-
- target = f"{module_name}.{fullname}" if fullname else module_name
- try:
- mod = sys.modules.get(module_name)
- if mod is None:
- mod = importlib.import_module(module_name)
- obj = mod
- if fullname:
- for part in fullname.split("."):
- obj = getattr(obj, part)
- mod_obj = inspect.getmodule(obj) or mod
- source_path = Path(inspect.getfile(mod_obj)).resolve()
- srcdir = Path(env.srcdir).resolve()
- try:
- source_path.relative_to(srcdir)
- return register_startup_file(env, docname, source_path), None
- except ValueError:
- return None, None
- except (AttributeError, ImportError, OSError, TypeError) as exc:
- logger.error(
- "Could not bootstrap autodoc REPL for %s: %s",
- target,
- exc,
- )
- return None, None
+def _autodoc_packages(app: Sphinx) -> str | None:
+ """Return configured package preload for autodoc doctest REPLs."""
+ return app.config.pyrepl_autodoc_packages or None
def _inside_autodoc_desc(node: nodes.Node) -> bool:
@@ -177,15 +154,27 @@ def transform_doctest_blocks(app: Sphinx, doctree: nodes.document):
source = doctest_to_replay_source(node.astext())
if not source.strip():
continue
- bootstrap_src = None
packages = None
+ bootstrap_src = None
desc = _find_autodoc_desc(node)
if desc is not None:
- bootstrap_src, packages = _resolve_autodoc_bootstrap(
- app, env, docname, desc
- )
- replay_src = register_autodoc_repl(env, docname, source)
- node.replace_self(make_pyrepl_raw(replay_src, bootstrap_src, packages))
+ packages = _autodoc_packages(app)
+ replay_src, replay_name = register_autodoc_repl(env, docname, source)
+ if desc is not None and packages:
+ sig = desc.next_node(addnodes.desc_signature)
+ if sig is not None:
+ bootstrap_text = autodoc_bootstrap_source(
+ sig.get("module"),
+ sig.get("fullname"),
+ desc.get("objtype"),
+ )
+ if bootstrap_text:
+ bootstrap_src = register_autodoc_bootstrap(
+ env, docname, bootstrap_text, replay_name
+ )
+ node.replace_self(
+ make_pyrepl_raw(replay_src, src=bootstrap_src, packages=packages)
+ )
replaced = True
if replaced:
@@ -258,7 +247,7 @@ def run(self):
if has_body:
body_text = doctest_to_replay_source(list(self.content))
- replay_src = register_autodoc_repl(env, env.docname, body_text)
+ replay_src, _ = register_autodoc_repl(env, env.docname, body_text)
attrs.append(f'replay-src="{replay_src}"')
self.env.metadata[self.env.docname]["pyrepl"] = True
@@ -303,6 +292,12 @@ def copy_asset_files(app, _):
for name, content in replay_files.items():
(replay_dest / name).write_text(content, encoding="utf-8")
+ raw_bootstrap = metadata.get(BOOTSTRAP_FILES_KEY)
+ if raw_bootstrap:
+ bootstrap_files = json.loads(raw_bootstrap)
+ for name, content in bootstrap_files.items():
+ (replay_dest / name).write_text(content, encoding="utf-8")
+
srcdir = Path(app.builder.srcdir)
copied = set()
for docname, metadata in app.env.metadata.items():
diff --git a/docs/_static/autodoc_demo.py b/tests/fixtures/pyrepl_test_pkg/pyrepl_test_pkg/demo.py
similarity index 99%
rename from docs/_static/autodoc_demo.py
rename to tests/fixtures/pyrepl_test_pkg/pyrepl_test_pkg/demo.py
index 09f42fb..dbc19f6 100644
--- a/docs/_static/autodoc_demo.py
+++ b/tests/fixtures/pyrepl_test_pkg/pyrepl_test_pkg/demo.py
@@ -1,5 +1,6 @@
"""Demo module for autodoc doctest REPL integration."""
+
def example_generator(n):
"""Generators yield values useful for iteration.
diff --git a/tests/fixtures/wheels/pyrepl_test_pkg-1.0.0-py3-none-any.whl b/tests/fixtures/wheels/pyrepl_test_pkg-1.0.0-py3-none-any.whl
index 527687e..d8a2685 100644
Binary files a/tests/fixtures/wheels/pyrepl_test_pkg-1.0.0-py3-none-any.whl and b/tests/fixtures/wheels/pyrepl_test_pkg-1.0.0-py3-none-any.whl differ
diff --git a/tests/test_autodoc_bootstrap.py b/tests/test_autodoc_bootstrap.py
index 1084478..c291a0a 100644
--- a/tests/test_autodoc_bootstrap.py
+++ b/tests/test_autodoc_bootstrap.py
@@ -1,72 +1,79 @@
import json
-import logging
+import shutil
import sys
from pathlib import Path
from unittest.mock import MagicMock
from sphinx.application import Sphinx
-from sphinx_pyrepl_web import _resolve_autodoc_bootstrap
+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 test_autodoc_bootstrap_uses_srcdir_module(tmp_path):
- srcdir = tmp_path / "docs"
- srcdir.mkdir()
- (srcdir / "_static").mkdir()
- (srcdir / "_static" / "demo.py").write_text(
- '''
-def greet():
- """Say hello.
+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
- Examples:
- >>> greet()
- 'hi'
- """
- return "hi"
-'''.strip()
- + "\n",
- encoding="utf-8",
- )
+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
-from pathlib import Path
-sys.path.insert(0, str(Path(__file__).resolve().parent / "_static"))
+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:: demo.greet\n", encoding="utf-8")
+ (srcdir / "index.rst").write_text(
+ ".. autofunction:: pyrepl_test_pkg.demo.example_generator\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()
+ app = _build_sphinx(srcdir, outdir, doctreedir)
html = (outdir / "index.html").read_text(encoding="utf-8")
- assert 'src="_static/demo.py"' in html
+ assert f'packages="{WHEEL_PATH}"' in html
assert 'replay-src="_static/pyrepl/index-1.py"' in html
- assert "pyrepl.js" in html
- assert (outdir / "_static" / "demo.py").is_file()
+ 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")
@@ -75,19 +82,26 @@ def greet():
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"
+ )
-
- assert list(replay_files) == ["index-1.py"]
+ 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_bootstrap_skips_installed_module(tmp_path):
+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(
'''
-from .core import Widget as _Widget
-
-class Widget(_Widget):
+class Widget:
"""A demo widget.
Example:
@@ -96,14 +110,62 @@ class Widget(_Widget):
'ready'
"""
- pass
+ label = "ready"
'''.strip()
+ "\n",
encoding="utf-8",
)
- (pkg_dir / "core.py").write_text(
+
+ 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",
@@ -128,60 +190,22 @@ class Widget:
)
(srcdir / "index.rst").write_text(".. autoclass:: installed_pkg.Widget\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)
html = (outdir / "index.html").read_text(encoding="utf-8")
assert 'replay-src="_static/pyrepl/index-1.py"' in html
- assert "", html.index("")]
assert 'packages="' not in pyrepl_tag
assert ' src="' not in pyrepl_tag
- replay_files = json.loads(
- app.env.metadata["index"].get("pyrepl-replay-files", "{}")
- )
- assert len(replay_files) == 1
- script = next(iter(replay_files.values()))
- assert script == "w = Widget()\n\nw.label\n"
-
-
-def test_bootstrap_failure_logs_error(caplog):
- caplog.set_level(logging.ERROR, logger="sphinx_pyrepl_web")
+def test_autodoc_packages_config_helper():
app = MagicMock()
- app.config.pyrepl_autodoc_bootstrap = True
-
- env = MagicMock()
- env.srcdir = "/tmp/docs"
- env.metadata = {"index": {}}
- env.note_dependency = MagicMock()
+ app.config.pyrepl_autodoc_packages = WHEEL_PATH
+ assert _autodoc_packages(app) == WHEEL_PATH
- sig = MagicMock()
- sig.get.side_effect = lambda key, default=None: {
- "module": "nonexistent_bootstrap_mod_xyz",
- "fullname": "missing",
- }.get(key, default)
+ app.config.pyrepl_autodoc_packages = ""
+ assert _autodoc_packages(app) is None
- desc = MagicMock()
- desc.next_node.return_value = sig
-
- result = _resolve_autodoc_bootstrap(app, env, "index", desc)
-
- assert result == (None, None)
- assert any(
- "Could not bootstrap autodoc REPL for nonexistent_bootstrap_mod_xyz.missing"
- in record.message
- for record in caplog.records
- )
+ 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
new file mode 100644
index 0000000..4a0e35b
--- /dev/null
+++ b/tests/test_autodoc_bootstrap_source.py
@@ -0,0 +1,29 @@
+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_include.py b/tests/test_autodoc_include.py
index 01464ce..500229c 100644
--- a/tests/test_autodoc_include.py
+++ b/tests/test_autodoc_include.py
@@ -91,10 +91,9 @@ def test_included_example_writes_all_replay_scripts(included_example_project):
assert len(replay_files) == 2
html = (outdir / "index.html").read_text(encoding="utf-8")
- assert 'src="_static/repl_include_demo.py"' in html
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}"
- assert (outdir / "_static" / "repl_include_demo.py").is_file()