From 7970e7a46cf255956757f6334fac5b9560f86315 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 27 Jun 2026 11:09:08 +0000 Subject: [PATCH 1/4] Fix bare doctest terminators in replay script generation strip_doctest_prompts() now drops bare '...' lines and '...' prompts followed only by whitespace, which are doctest block terminators with no Python semantics. Continuation prompts with code and explicit '>>> ...' ellipsis usage are unchanged. Adds unit and integration tests, and extends the Replay session docs example with a multi-line class definition. Fixes #7 Co-authored-by: chrizzftd --- docs/example.rst | 11 ++++++- sphinx_pyrepl_web/__init__.py | 15 +++++++-- tests/test_build_replay.py | 48 +++++++++++++++++++++++++++++ tests/test_strip_doctest_prompts.py | 31 +++++++++++++++++++ 4 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 tests/test_strip_doctest_prompts.py 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..cf4ae1f 100644 --- a/sphinx_pyrepl_web/__init__.py +++ b/sphinx_pyrepl_web/__init__.py @@ -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) are + dropped, since they carry no Python semantics. + """ result: list[str] = [] for line in lines: stripped = line.lstrip() if stripped.startswith(">>> "): result.append(stripped[4:]) elif stripped.startswith("... "): - result.append(stripped[4:]) + remainder = stripped[4:] + if remainder.strip() == "" and stripped != "... ": + continue + result.append(remainder) + elif stripped == "..." or ( + len(stripped) > 3 and stripped.startswith("...") and stripped[3:].strip() == "" + ): + continue else: result.append(line) return result diff --git a/tests/test_build_replay.py b/tests/test_build_replay.py index c7a6ce1..242cb77 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 script.strip().endswith("Foo()") diff --git a/tests/test_strip_doctest_prompts.py b/tests/test_strip_doctest_prompts.py new file mode 100644 index 0000000..84e3a1a --- /dev/null +++ b/tests/test_strip_doctest_prompts.py @@ -0,0 +1,31 @@ +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([">>> ..."]) == ["..."] From 13edafb05522959b4790a0d99ce49a5ed0f4da64 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 27 Jun 2026 11:14:54 +0000 Subject: [PATCH 2/4] Convert bare doctest terminators to blank lines in replay scripts A bare '...' marks the end of a multi-line REPL input block. Dropping it entirely caused the next statement to be parsed as a continuation, e.g. 'Foo()' after a class definition. Emit a blank line instead of Ellipsis or nothing. Co-authored-by: chrizzftd --- sphinx_pyrepl_web/__init__.py | 22 +++++++++++----------- tests/test_build_replay.py | 2 +- tests/test_strip_doctest_prompts.py | 3 ++- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/sphinx_pyrepl_web/__init__.py b/sphinx_pyrepl_web/__init__.py index cf4ae1f..520e2f4 100644 --- a/sphinx_pyrepl_web/__init__.py +++ b/sphinx_pyrepl_web/__init__.py @@ -29,23 +29,23 @@ def setup(app: Sphinx): def strip_doctest_prompts(lines: list[str]) -> list[str]: """Remove leading ``>>> `` / ``... `` prompts from doctest-style lines. - Bare ``...`` lines (doctest block terminators with no trailing content) are - dropped, since they carry no Python semantics. + 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("... "): - remainder = stripped[4:] - if remainder.strip() == "" and stripped != "... ": - continue - result.append(remainder) - elif stripped == "..." or ( - len(stripped) > 3 and stripped.startswith("...") and stripped[3:].strip() == "" - ): - continue + 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 242cb77..622bb94 100644 --- a/tests/test_build_replay.py +++ b/tests/test_build_replay.py @@ -131,4 +131,4 @@ def test_build_omits_bare_doctest_terminator(tmp_path): script = script_path.read_text(encoding="utf-8") assert "class Foo:" in script assert "\n...\n" not in script - assert script.strip().endswith("Foo()") + assert " x = 1\n\nFoo()" in script diff --git a/tests/test_strip_doctest_prompts.py b/tests/test_strip_doctest_prompts.py index 84e3a1a..088fef0 100644 --- a/tests/test_strip_doctest_prompts.py +++ b/tests/test_strip_doctest_prompts.py @@ -11,12 +11,13 @@ def test_issue_7_multiline_class_with_bare_terminator(): assert strip_doctest_prompts(lines) == [ "class Foo:", " x = 1", + "", "Foo()", ] def test_bare_terminator_with_trailing_whitespace(): - assert strip_doctest_prompts([" ... "]) == [] + assert strip_doctest_prompts([" ... "]) == [""] def test_continuation_line_preserved(): From e960c35c157bb2bd11ad42c8f09ef6cc6afd8eb2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 27 Jun 2026 11:19:23 +0000 Subject: [PATCH 3/4] Prepare release 0.1.1 Bump version, add CHANGELOG, and add PyPI publish workflow triggered by GitHub releases. Co-authored-by: chrizzftd --- .github/workflows/publish.yml | 26 ++++++++++++++++++++++++++ CHANGELOG.md | 24 ++++++++++++++++++++++++ sphinx_pyrepl_web/__init__.py | 2 +- 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/publish.yml create mode 100644 CHANGELOG.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..51c7402 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,26 @@ +# Publish to PyPI when a GitHub release is published. +# Requires a PyPI trusted publisher for this workflow, or a PYPI_API_TOKEN secret. + +name: Publish + +on: + release: + types: [published] + +permissions: + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - name: Install build backend + run: python -m pip install --upgrade pip flit + - name: Build package + run: flit build + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b8e19cd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.1] - 2026-06-27 + +### Fixed + +- Bare `...` doctest block terminators in `.. py-repl::` directive bodies are no + longer emitted as Python `Ellipsis` in generated replay scripts. They are + converted to blank lines so multi-line blocks (e.g. class definitions) replay + correctly in pyrepl-web. ([#7](https://github.com/chrizzFTD/sphinx-pyrepl-web/issues/7)) + +### Changed + +- Extended the Replay session docs example with a multi-line class definition + demonstrating standard doctest formatting. + +## [0.1.0] - 2026-06-27 + +Initial PyPI release. diff --git a/sphinx_pyrepl_web/__init__.py b/sphinx_pyrepl_web/__init__.py index 520e2f4..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 From f23c84ab07d7dde7ecb3d46bad683875a3db25da Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 27 Jun 2026 11:21:15 +0000 Subject: [PATCH 4/4] Remove CHANGELOG and publish workflow; keep version bump only Co-authored-by: chrizzftd --- .github/workflows/publish.yml | 26 -------------------------- CHANGELOG.md | 24 ------------------------ 2 files changed, 50 deletions(-) delete mode 100644 .github/workflows/publish.yml delete mode 100644 CHANGELOG.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 51c7402..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Publish to PyPI when a GitHub release is published. -# Requires a PyPI trusted publisher for this workflow, or a PYPI_API_TOKEN secret. - -name: Publish - -on: - release: - types: [published] - -permissions: - id-token: write - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-python@v6 - with: - python-version: "3.12" - - name: Install build backend - run: python -m pip install --upgrade pip flit - - name: Build package - run: flit build - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index b8e19cd..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,24 +0,0 @@ -# Changelog - -All notable changes to this project are documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [0.1.1] - 2026-06-27 - -### Fixed - -- Bare `...` doctest block terminators in `.. py-repl::` directive bodies are no - longer emitted as Python `Ellipsis` in generated replay scripts. They are - converted to blank lines so multi-line blocks (e.g. class definitions) replay - correctly in pyrepl-web. ([#7](https://github.com/chrizzFTD/sphinx-pyrepl-web/issues/7)) - -### Changed - -- Extended the Replay session docs example with a multi-line class definition - demonstrating standard doctest formatting. - -## [0.1.0] - 2026-06-27 - -Initial PyPI release.