diff --git a/docs/example.rst b/docs/example.rst index 484a69f..d254db6 100644 --- a/docs/example.rst +++ b/docs/example.rst @@ -48,7 +48,8 @@ Replay session -------------- Inline directive content is replayed with ``>>>`` prompts, syntax highlighting, -and live output. Doctest-style ``>>>`` prefixes are stripped automatically. +and live output. Doctest-style ``>>>`` / ``...`` prefixes and bare ``...`` +block terminators are stripped automatically. .. code-block:: rst @@ -59,6 +60,10 @@ and live output. Doctest-style ``>>>`` prefixes are stripped automatically. >>> x = 2 + 2 >>> print(f"{x=}") >>> x * 10 + >>> class Foo: + ... x = 1 + ... + >>> Foo() .. py-repl:: :no-header: @@ -67,6 +72,10 @@ and live output. Doctest-style ``>>>`` prefixes are stripped automatically. >>> x = 2 + 2 >>> print(f"{x=}") >>> x * 10 + >>> class Foo: + ... x = 1 + ... + >>> Foo() Combine a silent bootstrap file with a visible replay body: diff --git a/sphinx_pyrepl_web/__init__.py b/sphinx_pyrepl_web/__init__.py index 7b8aaa0..24ea698 100644 --- a/sphinx_pyrepl_web/__init__.py +++ b/sphinx_pyrepl_web/__init__.py @@ -1,6 +1,6 @@ """A Sphinx extension for embedding pyrepl-web Python REPLs in documentation.""" -__version__ = "0.1.0" +__version__ = "0.1.1" import json from pathlib import Path @@ -27,14 +27,25 @@ def setup(app: Sphinx): def strip_doctest_prompts(lines: list[str]) -> list[str]: - """Remove leading ``>>> `` / ``... `` prompts from doctest-style lines.""" + """Remove leading ``>>> `` / ``... `` prompts from doctest-style lines. + + Bare ``...`` lines (doctest block terminators with no trailing content) become + blank lines, since they mark the end of a multi-line input block in the REPL. + """ result: list[str] = [] for line in lines: stripped = line.lstrip() if stripped.startswith(">>> "): result.append(stripped[4:]) - elif stripped.startswith("... "): - result.append(stripped[4:]) + elif stripped.startswith("..."): + if stripped.startswith("... "): + remainder = stripped[4:] + else: + remainder = stripped[3:] + if remainder.strip() == "": + result.append("") + else: + result.append(remainder) else: result.append(line) return result diff --git a/tests/test_build_replay.py b/tests/test_build_replay.py index c7a6ce1..622bb94 100644 --- a/tests/test_build_replay.py +++ b/tests/test_build_replay.py @@ -84,3 +84,51 @@ 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( + "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_strip_doctest_prompts.py b/tests/test_strip_doctest_prompts.py new file mode 100644 index 0000000..088fef0 --- /dev/null +++ b/tests/test_strip_doctest_prompts.py @@ -0,0 +1,32 @@ +from sphinx_pyrepl_web import strip_doctest_prompts + + +def test_issue_7_multiline_class_with_bare_terminator(): + lines = [ + ">>> class Foo:", + "... x = 1", + "...", + ">>> Foo()", + ] + assert strip_doctest_prompts(lines) == [ + "class Foo:", + " x = 1", + "", + "Foo()", + ] + + +def test_bare_terminator_with_trailing_whitespace(): + assert strip_doctest_prompts([" ... "]) == [""] + + +def test_continuation_line_preserved(): + assert strip_doctest_prompts(["... x = 1"]) == [" x = 1"] + + +def test_single_line_prompts(): + assert strip_doctest_prompts([">>> x = 1", ">>> x + 1"]) == ["x = 1", "x + 1"] + + +def test_explicit_ellipsis(): + assert strip_doctest_prompts([">>> ..."]) == ["..."]