diff --git a/README.md b/README.md index d66e548..d7d223b 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,9 @@ Sphinx extension to embed [pyrepl-web](https://github.com/chrizzFTD/pyrepl-web) pip install sphinx-pyrepl-web ``` -For development: - -```bash -pip install -e ".[test,docs]" -``` - ## Usage -Add the extension to the target project's `conf.py`: +Add the extension to the target project's `conf.py` module: ```python extensions = [ @@ -46,68 +40,53 @@ Embed a REPL with the `py-repl` directive: ### Directive options -All options drive [pyrepl-web](https://github.com/chrizzFTD/pyrepl-web)'s attributes, with the exception of `silent`: +All options drive [pyrepl-web](https://github.com/chrizzFTD/pyrepl-web)'s attributes: -| Option | Description | -|--------|-------------| -| `:theme:` | Color theme (`catppuccin-mocha`, `catppuccin-latte`) | -| `:packages:` | Comma-separated PyPI packages to preload | -| `:repl-title:` | Title in the REPL header | -| `:src:` | Path to a Python startup script | +| Option | Description | +|--------|----------------------------------------------------------------| +| `:theme:` | Color theme (`catppuccin-mocha`, `catppuccin-latte`) | +| `:packages:` | Comma-separated PyPI packages, URLs or relative wheel paths | +| `:repl-title:` | Title in the REPL header | +| `:src:` | Path to a Python startup script | | `:replay:` | Replay `:src:` with interactive prompts instead of silent load | -| `:silent:` | Keep `:src:` silent even when combined with a directive body | -| `:no-header:` | Hide the header bar | -| `:no-buttons:` | Hide copy/clear buttons | -| `:readonly:` | Disable input | -| `:no-banner:` | Hide the Python version banner | +| `:no-header:` | Hide the header bar | +| `:no-buttons:` | Hide copy/clear buttons | +| `:readonly:` | Disable input | +| `:no-banner:` | Hide the Python version banner | -Python code within the `.. py-repl::` directive is written to `_static/pyrepl/` at build time and emitted as `replay-src`. +### Sphinx options -Optional Sphinx config: +Enable [doctest style examples](https://docs.python.org/3/library/doctest.html) conversion into pre-configured interactive REPLs with: -```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_doctest_blocks`: -### Docstring conversion +| Value | Outcome | +|-------------------|---------------------------------------| +| `False` (default) | Don't convert doctest blocks | +| `"autodoc"` | Convert doctest blocks from `autodoc` | +| `"all"` | Convert all doctest blocks | -Converting doctest examples from docstrings into interactive REPLs is opt-in with `sphinx.ext.autodoc`: +`pyrepl_autodoc_packages`: + +| Value | Outcome | +|-------------------------|--------------------------------------------------------------------| +| `None` (default) | Replay doctest input without preloading packages | +| Wheel path / PyPI names | Install the package and import the documented object before replay | + +### Local wheels + +Unreleased package wheels can be available in the REPL by building them under Sphinx's `html_static_path`. + +All options combined: ```python -# conf.py extensions = [ "sphinx.ext.autodoc", "sphinx_pyrepl_web", ] -pyrepl_doctest_blocks = "autodoc" -``` -| | `pyrepl_doctest_blocks` options | -|-------------------|-------------------------------------| -| `False` (default) | Disable autodoc conversion | -| `"autodoc"` | Convert doctests found by autodoc | -| `"all"` | Transform every doctest block found | +html_static_path = ["_static"] - -| | `pyrepl_autodoc_bootstrap` options | -|------------------|------------------------------------------------------------------------------| -| `True` (default) | Bootstrap REPL: in-tree modules via silent `:src:`, packages via `packages=` | -| `False` | Replay doctest input only; documented names are not pre-defined | - -## Updating pyrepl-web - -Since [chrizzFTD/pyrepl-web](https://github.com/chrizzFTD/pyrepl-web) is a fork, this sphinx extension vendors the JavaScript assets for easier distribution. To update them, run: - -```bash -python scripts/vendor_repl.py -``` - -The `grill` branch is used by default. Use the `branch` argument to specify a different one: - -```bash -python scripts/vendor_repl.py --branch custom/feature-branch +pyrepl_doctest_blocks = "autodoc" +pyrepl_autodoc_packages = "_static/wheels/my_package-1.0.0-py3-none-any.whl" ``` - -This requires [git](https://git-scm.com/) and [Bun](https://bun.sh/). 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 new file mode 100644 index 0000000..d8a2685 Binary files /dev/null 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 5ee02fd..9980427 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,7 +14,9 @@ "sphinx_pyrepl_web", ] pyrepl_doctest_blocks = "autodoc" -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] +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", "example.rst"] html_sidebars = { "**": [ diff --git a/docs/development.rst b/docs/development.rst new file mode 100644 index 0000000..5790472 --- /dev/null +++ b/docs/development.rst @@ -0,0 +1,85 @@ +Development +=========== + +Setup +----- + +Clone the repository, then install in editable mode with test and docs dependencies: + +.. code-block:: bash + + pip install -e ".[test,docs]" + +The ``[docs]`` extra pulls in doc build dependencies and the ``pyrepl_test_pkg`` +fixture used in the examples. + +Build and preview docs +---------------------- + +Build HTML output from the ``docs/`` directory: + +.. code-block:: bash + + python -m sphinx -b html docs docs/_build + +REPLs load JavaScript, replay scripts, and wheels with ``fetch``, so they do not +work when opening ``index.html`` directly from disk (``file://`` URLs). Serve the +build output over HTTP instead: + +.. code-block:: bash + + python -m http.server --directory docs/_build + +Then open http://localhost:8000/ in a browser. + +Build-time behavior +------------------- + +Python code within a ``.. py-repl::`` directive is written to ``_static/pyrepl/`` +at build time and emitted as ``replay-src``. + +File paths identifiers in ``:packages:``, ``:src:``, ``replay-src``, and +``pyrepl_autodoc_packages`` are rewritten to page-relative URLs so REPLs work on +nested pages (for example ``docs/api/...``). PyPI package names, absolute URLs, +and paths written as root-absolute (``/_static/...``) are left unchanged. + +``pyrepl_js`` (default: ``"../pyrepl.js"``) sets the loader script Sphinx injects +on REPL pages. The extension vendors and copies `pyrepl-web `_ automatically; +override this only when pointing at a custom loader path or CDN. + +Static wheels +------------- + +Wheel packages must be `Pyodide `_ compatible (pure-python packages work out of the box). +For ``CPython`` extensions, visit `pyodide-build `_. + +Wheels under ``_static/`` are copied into the HTML output when ``_static`` is +listed in ``html_static_path``. At runtime, `pyrepl-web `_ +resolves site-relative wheel paths to absolute URLs before calling +``micropip.install()``. + +Paths must use the wheel's actual PyPI-compliant filename (for example +``myext-1.2.3-py3-none-any.whl``). ``micropip`` rejects other names. + +Ensure the web server serves ``.whl`` files with MIME type ``application/zip`` +(Read the Docs does this by default). + +Updating pyrepl-web +------------------- + +This extension vendors JavaScript from +`pyrepl-web `_'s fork for easier +distribution. To refresh the vendored assets: + +.. code-block:: bash + + python scripts/vendor_repl.py + +The ``grill`` branch is used by default. Pass ``--branch`` to vendor from another +branch: + +.. code-block:: bash + + python scripts/vendor_repl.py --branch custom/feature-branch + +This requires `git `_ and `Bun `_. diff --git a/docs/example.rst b/docs/example.rst index 3dccde5..bfc7464 100644 --- a/docs/example.rst +++ b/docs/example.rst @@ -51,7 +51,7 @@ Rendered result: Replay session -------------- -Inline directive content should follow Doctest-style (``>>>`` / ``...``) and is used as replay prompts. +Inline content should follow `doctest-style `_ (``>>>`` / ``...``) and is used as replay prompts. .. code-block:: rst @@ -79,7 +79,7 @@ Inline directive content should follow Doctest-style (``>>>`` / ``...``) and is ... >>> Foo() -Combine a silent bootstrap file with a visible replay body: +Combine a startup script with a visible replay body: .. code-block:: rst @@ -120,24 +120,55 @@ Rendered result: :no-header: :no-banner: +Local packages +-------------- + +Use static local wheel packages on ``.. py-repl::`` directives: + +.. code-block:: rst + + .. py-repl:: + :packages: _static/wheels/pyrepl_test_pkg-1.0.0-py3-none-any.whl + :no-header: + :no-banner: + + >>> import pyrepl_test_pkg + >>> pyrepl_test_pkg.ping() + +.. py-repl:: + :packages: _static/wheels/pyrepl_test_pkg-1.0.0-py3-none-any.whl + :no-header: + :no-banner: + + >>> import pyrepl_test_pkg + >>> pyrepl_test_pkg.ping() + + Autodoc ------- -The documented module's source is loaded in advance before replay, so -module members are available in the REPL namespace. Modules under the Sphinx -source tree use silent ``:src:``; installed packages use ``packages=``. +Use ``pyrepl_doctest_blocks = "autodoc"`` to turn docstrings from ``autodoc`` into interactive REPL examples. + +Set ``pyrepl_autodoc_packages`` to install the documented package and automatically import the documented object before replay: + +.. code-block:: python + + # conf.py + html_static_path = ["_static"] + pyrepl_doctest_blocks = "autodoc" + pyrepl_autodoc_packages = "_static/wheels/pyrepl_test_pkg-1.0.0-py3-none-any.whl" 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 diff --git a/docs/index.rst b/docs/index.rst index 6744fad..069fb1a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,5 +3,7 @@ .. include:: example.rst +.. include:: development.rst + .. toctree:: :maxdepth: 2 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 95eaf82..9997be9 100644 --- a/sphinx_pyrepl_web/__init__.py +++ b/sphinx_pyrepl_web/__init__.py @@ -1,34 +1,60 @@ """A Sphinx extension for embedding pyrepl-web Python REPLs in documentation.""" -__version__ = "0.2.0" +__version__ = "0.3.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 from sphinx import addnodes from sphinx.application import Sphinx +from sphinx.builders import Builder from sphinx.util import logging from sphinx.util.docutils import SphinxDirective from sphinx.util.fileutil import copy_asset_file +from sphinx.util.osutil import relative_uri 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__) +_ABSOLUTE_PATH_PREFIXES = ("/", "http://", "https://", "emfs:") + + +def _is_file_like_path(path: str) -> bool: + """Return True if *path* should be rewritten as a page-relative asset URL.""" + if path.startswith(_ABSOLUTE_PATH_PREFIXES): + return False + if " @ " in path: + return False + return "/" in path or path.endswith((".whl", ".py")) + + +def _asset_href(builder: Builder, docname: str, path: str) -> str: + """Rewrite a file path for the HTML page that will emit it.""" + if not _is_file_like_path(path): + return path + if builder.format != "html": + return path + return relative_uri(builder.get_target_uri(docname), path) + + +def _asset_href_packages(builder: Builder, docname: str, packages: str) -> str: + """Rewrite comma-separated package entries that refer to local files.""" + return ", ".join( + _asset_href(builder, docname, part.strip()) for part in packages.split(",") + ) 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_doctest_blocks", False, "env", types=(bool, str)) + 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 +85,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,34 +94,63 @@ 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( + builder: Builder, + docname: str, 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}"'] + attrs = [ + "no-header", + "no-banner", + f'replay-src="{_asset_href(builder, docname, replay_src)}"', + ] if packages: - attrs.insert(0, f'packages="{packages}"') + attrs.insert( + 0, f'packages="{_asset_href_packages(builder, docname, packages)}"' + ) if src: - attrs.insert(0, f'src="{src}"') + attrs.insert(0, f'src="{_asset_href(builder, docname, src)}"') attr_str = " ".join(attrs) return nodes.raw("", f"\n", format="html") @@ -110,46 +165,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.""" - 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, module_name.split(".")[0] - 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: @@ -172,15 +190,29 @@ 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 + 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( + app.builder, docname, replay_src, src=bootstrap_src, packages=packages ) - replay_src = register_autodoc_repl(env, docname, source) - node.replace_self(make_pyrepl_raw(replay_src, bootstrap_src, packages)) + ) replaced = True if replaced: @@ -202,11 +234,12 @@ class PyRepl(SphinxDirective): "readonly": directives.flag, "no-banner": directives.flag, "replay": directives.flag, - "silent": directives.flag, } def run(self): env = self.env + builder = env._app.builder + docname = env.docname attrs: list[str] = [] for option, attr in ( @@ -216,6 +249,8 @@ def run(self): ): if option in self.options: value = self.options[option] + if option == "packages": + value = _asset_href_packages(builder, docname, value) attrs.append(f'{attr}="{value}"') for flag in ("no-header", "no-buttons", "readonly", "no-banner"): @@ -224,7 +259,6 @@ def run(self): has_body = bool(self.content) force_replay = "replay" in self.options - force_silent = "silent" in self.options if "src" in self.options: _, abs_path = self.env.relfn2path(self.options["src"]) @@ -234,7 +268,11 @@ def run(self): except OSError as exc: raise self.error(f"Could not read file: {exc}") from exc self.env.note_dependency(path) - rel_src = path.relative_to(Path(self.env.srcdir)).as_posix() + rel_src = _asset_href( + builder, + docname, + path.relative_to(Path(self.env.srcdir)).as_posix(), + ) startup_files = json.loads( self.env.metadata[self.env.docname].setdefault( STARTUP_FILES_KEY, "[]" @@ -245,16 +283,14 @@ def run(self): startup_files ) + attrs.append(f'src="{rel_src}"') if force_replay and not has_body: - attrs.append(f'src="{rel_src}"') attrs.append("replay") - elif not (force_silent and not has_body): - attrs.append(f'src="{rel_src}"') if has_body: body_text = doctest_to_replay_source(list(self.content)) - replay_src = register_autodoc_repl(env, env.docname, body_text) - attrs.append(f'replay-src="{replay_src}"') + replay_src, _ = register_autodoc_repl(env, docname, body_text) + attrs.append(f'replay-src="{_asset_href(builder, docname, replay_src)}"') self.env.metadata[self.env.docname]["pyrepl"] = True attr_str = (" " + " ".join(attrs)) if attrs else "" @@ -298,6 +334,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/sphinx_pyrepl_web/pyrepl/chunk-e4mhg83d.js b/sphinx_pyrepl_web/pyrepl/chunk-cnc6ympw.js similarity index 99% rename from sphinx_pyrepl_web/pyrepl/chunk-e4mhg83d.js rename to sphinx_pyrepl_web/pyrepl/chunk-cnc6ympw.js index 108bd2a..c61b807 100644 --- a/sphinx_pyrepl_web/pyrepl/chunk-e4mhg83d.js +++ b/sphinx_pyrepl_web/pyrepl/chunk-cnc6ympw.js @@ -1,3 +1,3 @@ -import{i as $}from"./chunk-ftjk4vft.js";var __dirname="/tmp/vendor-pyrepl-oxf1jz4v/pyrepl-web/node_modules/pyodide";var O0=Object.defineProperty,H=(G,Q)=>O0(G,"name",{value:Q,configurable:!0}),m=((G)=>$)(function(G){return $.apply(this,arguments)}),R0=(()=>{for(var G=new Uint8Array(128),Q=0;Q<64;Q++)G[Q<26?Q+65:Q<52?Q+71:Q<62?Q-4:Q*4-205]=Q;return(W)=>{for(var z=W.length,Y=new Uint8Array((z-(W[z-1]=="=")-(W[z-2]=="="))*3/4|0),X=0,B=0;X>4,Y[B++]=K<<4|M>>2,Y[B++]=M<<6|V}return Y}})();function p(G){return!isNaN(parseFloat(G))&&isFinite(G)}H(p,"_isNumber");function T(G){return G.charAt(0).toUpperCase()+G.substring(1)}H(T,"_capitalize");function g(G){return function(){return this[G]}}H(g,"_getter");var R=["isConstructor","isEval","isNative","isToplevel"],U=["columnNumber","lineNumber"],F=["fileName","functionName","source"],U0=["args"],F0=["evalOrigin"],S=R.concat(U,F,U0,F0);function j(G){if(G)for(var Q=0;QO0(G,"name",{value:Q,configurable:!0}),m=((G)=>$)(function(G){return $.apply(this,arguments)}),R0=(()=>{for(var G=new Uint8Array(128),Q=0;Q<64;Q++)G[Q<26?Q+65:Q<52?Q+71:Q<62?Q-4:Q*4-205]=Q;return(W)=>{for(var z=W.length,Y=new Uint8Array((z-(W[z-1]=="=")-(W[z-2]=="="))*3/4|0),X=0,B=0;X>4,Y[B++]=K<<4|M>>2,Y[B++]=M<<6|V}return Y}})();function p(G){return!isNaN(parseFloat(G))&&isFinite(G)}H(p,"_isNumber");function T(G){return G.charAt(0).toUpperCase()+G.substring(1)}H(T,"_capitalize");function g(G){return function(){return this[G]}}H(g,"_getter");var R=["isConstructor","isEval","isNative","isToplevel"],U=["columnNumber","lineNumber"],F=["fileName","functionName","source"],U0=["args"],F0=["evalOrigin"],S=R.concat(U,F,U0,F0);function j(G){if(G)for(var Q=0;Q-1&&(Y=Y.replace(/eval code/g,"eval").replace(/(\(eval at [^()]*)|(,.*$)/g,""));var X=Y.replace(/^\s+/,"").replace(/\(eval code/g,"(").replace(/^.*?\s+/,""),B=X.match(/ (\(.+\)$)/);X=B?X.replace(B[0],""):X;var J=this.extractLocation(B?B[1]:X),K=B&&X||void 0,M=["eval",""].indexOf(J[0])>-1?void 0:J[0];return new w({functionName:K,fileName:M,lineNumber:J[1],columnNumber:J[2],source:Y})},this)},"ErrorStackParser$$parseV8OrIE"),parseFFOrSafari:H(function(W){var z=W.stack.split(` `).filter(function(Y){return!Y.match(Q)},this);return z.map(function(Y){if(Y.indexOf(" > eval")>-1&&(Y=Y.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g,":$1")),Y.indexOf("@")===-1&&Y.indexOf(":")===-1)return new w({functionName:Y});var X=/((.*".+"[^@]*)?[^@]*)(?:@)/,B=Y.match(X),J=B&&B[1]?B[1]:void 0,K=this.extractLocation(Y.replace(X,""));return new w({functionName:J,fileName:K[0],lineNumber:K[1],columnNumber:K[2],source:Y})},this)},"ErrorStackParser$$parseFFOrSafari")}}H(u,"ErrorStackParser");var k0=new u,I0=k0;function l(){if(typeof API<"u"&&API!==globalThis.API)return API.runtimeEnv;let G=typeof Bun<"u",Q=typeof Deno<"u",W=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&!1,z=typeof navigator=="object"&&typeof navigator.userAgent=="string"&&navigator.userAgent.indexOf("Chrome")===-1&&navigator.userAgent.indexOf("Safari")>-1,Y=typeof read=="function"&&typeof load=="function",X=typeof navigator=="object"&&navigator.userAgent?.includes("Cloudflare-Workers");return d({IN_BUN:G,IN_DENO:Q,IN_NODE:W,IN_SAFARI:z,IN_SHELL:Y,IN_WORKERD:X})}H(l,"getGlobalRuntimeEnv");var x=l();function d(G){let Q=G.IN_NODE&&typeof f<"u"&&P0&&typeof m=="function"&&typeof __dirname=="string",W=G.IN_NODE&&!Q,z=!G.IN_NODE&&!G.IN_DENO&&!G.IN_BUN,Y=z&&typeof window<"u"&&typeof window.document<"u"&&typeof document.createElement=="function"&&"sessionStorage"in window&&typeof globalThis.importScripts!="function",X=z&&typeof globalThis.WorkerGlobalScope<"u"&&typeof globalThis.self<"u"&&globalThis.self instanceof globalThis.WorkerGlobalScope;if(X&&c())throw Error("Classic web workers are not supported");let B={...G,IN_BROWSER:z,IN_BROWSER_MAIN_THREAD:Y,IN_BROWSER_WEB_WORKER:X,IN_NODE_COMMONJS:Q,IN_NODE_ESM:W};if(!(B.IN_BROWSER_MAIN_THREAD||B.IN_BROWSER_WEB_WORKER||B.IN_NODE||B.IN_SHELL||B.IN_WORKERD))throw Error(`Cannot determine runtime environment: ${JSON.stringify(B)}`);return B}H(d,"calculateDerivedFlags");function c(){try{return globalThis.importScripts("data:text/javascript,"),!0}catch{return!1}}H(c,"isClassicWorker");var s,E,N0,_,h;async function y(){if(!x.IN_NODE||(s=(await import("./chunk-vx91qfkd.js")).default,_=await import("node:fs"),h=await import("node:fs/promises"),N0=(await import("node:vm")).default,E=await import("./chunk-94xkfg72.js"),v=E.sep,typeof m<"u"))return;let G=_,Q=await import("./chunk-x20ze186.js"),W=await import("ws"),z=await import("node:child_process"),Y={fs:G,crypto:Q,ws:W,child_process:z};globalThis.require=function(X){return Y[X]}}H(y,"initNodeModules");function a(G,Q){return E.resolve(Q||".",G)}H(a,"node_resolvePath");function i(G,Q){return Q===void 0&&(Q=location),new URL(G,Q).toString()}H(i,"browser_resolvePath");var I;x.IN_NODE?I=a:x.IN_SHELL?I=H((G)=>G,"resolvePath"):I=i;var v;x.IN_NODE||(v="/");function o(G,Q){return G.startsWith("file://")&&(G=G.slice(7)),G.includes("://")?{response:fetch(G)}:{binary:h.readFile(G).then((W)=>new Uint8Array(W.buffer,W.byteOffset,W.byteLength))}}H(o,"node_getBinaryResponse");function n(G,Q){if(G.startsWith("file://")&&(G=G.slice(7)),G.includes("://"))throw Error("Shell cannot fetch urls");return{binary:Promise.resolve(new Uint8Array(readbuffer(G)))}}H(n,"shell_getBinaryResponse");function r(G,Q){let W=new URL(G,location);return{response:fetch(W,Q?{integrity:Q}:{})}}H(r,"browser_getBinaryResponse");var N;x.IN_NODE?N=o:x.IN_SHELL?N=n:N=r;async function t(G,Q){let{response:W,binary:z}=N(G,Q);if(z)return z;let Y=await W;if(!Y.ok)throw Error(`Failed to load '${G}': request failed.`);return new Uint8Array(await Y.arrayBuffer())}H(t,"loadBinaryFile");var b;x.IN_NODE?b=e:b=H(async(G)=>await import(G),"loadScript");async function e(G){return G.startsWith("file://")&&(G=G.slice(7)),G.includes("://")?await import(G):await import(s.pathToFileURL(G).href)}H(e,"nodeLoadScript");async function G0(G){if(x.IN_NODE){await y();let Q=await h.readFile(G,{encoding:"utf8"});return JSON.parse(Q)}else if(x.IN_SHELL){let Q=read(G);return JSON.parse(Q)}else return await(await fetch(G)).json()}H(G0,"loadLockFile");async function Q0(){if(x.IN_NODE_COMMONJS)return __dirname;let G;try{throw Error()}catch(z){G=z}let Q=I0.parse(G)[0].fileName;if(x.IN_NODE&&!Q.startsWith("file://")&&(Q=`file://${Q}`),x.IN_NODE_ESM){let z=await import("./chunk-94xkfg72.js");return(await import("./chunk-vx91qfkd.js")).fileURLToPath(z.dirname(Q))}let W=Q.lastIndexOf(v);if(W===-1)throw Error("Could not extract indexURL path from pyodide module location. Please pass the indexURL explicitly to loadPyodide.");return Q.slice(0,W)}H(Q0,"calculateDirname");function W0(G){return G.substring(0,G.lastIndexOf("/")+1)||globalThis.location?.toString()||"."}H(W0,"calculateInstallBaseUrl");function X0(G){let Q=G.FS,W=G.FS.filesystems.MEMFS,z=G.PATH,Y={DIR_MODE:16895,FILE_MODE:33279,mount:H(function(X){if(!X.opts.fileSystemHandle)throw Error("opts.fileSystemHandle is required");return W.mount.apply(null,arguments)},"mount"),syncfs:H(async(X,B,J)=>{try{let K=Y.getLocalSet(X),M=await Y.getRemoteSet(X),V=B?M:K,C=B?K:M;await Y.reconcile(X,V,C),J(null)}catch(K){J(K)}},"syncfs"),getLocalSet:H((X)=>{let B=Object.create(null);function J(V){return V!=="."&&V!==".."}H(J,"isRealDir");function K(V){return(C)=>z.join2(V,C)}H(K,"toAbsolute");let M=Q.readdir(X.mountpoint).filter(J).map(K(X.mountpoint));for(;M.length;){let V=M.pop(),C=Q.stat(V);Q.isDir(C.mode)&&M.push.apply(M,Q.readdir(V).filter(J).map(K(V))),B[V]={timestamp:C.mtime,mode:C.mode}}return{type:"local",entries:B}},"getLocalSet"),getRemoteSet:H(async(X)=>{let B=Object.create(null),J=await S0(X.opts.fileSystemHandle);for(let[K,M]of J)K!=="."&&(B[z.join2(X.mountpoint,K)]={timestamp:M.kind==="file"?new Date((await M.getFile()).lastModified):new Date,mode:M.kind==="file"?Y.FILE_MODE:Y.DIR_MODE});return{type:"remote",entries:B,handles:J}},"getRemoteSet"),loadLocalEntry:H((X)=>{let B=Q.lookupPath(X,{}).node,J=Q.stat(X);if(Q.isDir(J.mode))return{timestamp:J.mtime,mode:J.mode};if(Q.isFile(J.mode))return B.contents=W.getFileDataAsTypedArray(B),{timestamp:J.mtime,mode:J.mode,contents:B.contents};throw Error("node type not supported")},"loadLocalEntry"),storeLocalEntry:H((X,B)=>{if(Q.isDir(B.mode))Q.mkdirTree(X,B.mode);else if(Q.isFile(B.mode))Q.writeFile(X,B.contents,{canOwn:!0});else throw Error("node type not supported");Q.chmod(X,B.mode),Q.utime(X,B.timestamp,B.timestamp)},"storeLocalEntry"),removeLocalEntry:H((X)=>{var B=Q.stat(X);Q.isDir(B.mode)?Q.rmdir(X):Q.isFile(B.mode)&&Q.unlink(X)},"removeLocalEntry"),loadRemoteEntry:H(async(X)=>{if(X.kind==="file"){let B=await X.getFile();return{contents:new Uint8Array(await B.arrayBuffer()),mode:Y.FILE_MODE,timestamp:new Date(B.lastModified)}}else{if(X.kind==="directory")return{mode:Y.DIR_MODE,timestamp:new Date};throw Error("unknown kind: "+X.kind)}},"loadRemoteEntry"),storeRemoteEntry:H(async(X,B,J)=>{let K=X.get(z.dirname(B)),M=Q.isFile(J.mode)?await K.getFileHandle(z.basename(B),{create:!0}):await K.getDirectoryHandle(z.basename(B),{create:!0});if(M.kind==="file"){let V=await M.createWritable();await V.write(J.contents),await V.close()}X.set(B,M)},"storeRemoteEntry"),removeRemoteEntry:H(async(X,B)=>{await X.get(z.dirname(B)).removeEntry(z.basename(B)),X.delete(B)},"removeRemoteEntry"),reconcile:H(async(X,B,J)=>{let K=0,M=[];Object.keys(B.entries).forEach(function(Z){let q=B.entries[Z],O=J.entries[Z];(!O||Q.isFile(q.mode)&&q.timestamp.getTime()>O.timestamp.getTime())&&(M.push(Z),K++)}),M.sort();let V=[];if(Object.keys(J.entries).forEach(function(Z){B.entries[Z]||(V.push(Z),K++)}),V.sort().reverse(),!K)return;let C=B.type==="remote"?B.handles:J.handles;for(let Z of M){let q=z.normalize(Z.replace(X.mountpoint,"/")).substring(1);if(J.type==="local"){let O=C.get(q),A0=await Y.loadRemoteEntry(O);Y.storeLocalEntry(Z,A0)}else{let O=Y.loadLocalEntry(Z);await Y.storeRemoteEntry(C,q,O)}}for(let Z of V)if(J.type==="local")Y.removeLocalEntry(Z);else{let q=z.normalize(Z.replace(X.mountpoint,"/")).substring(1);await Y.removeRemoteEntry(C,q)}},"reconcile")};G.FS.filesystems.NATIVEFS_ASYNC=Y}H(X0,"initializeNativeFS");var S0=H(async(G)=>{let Q=[];async function W(Y){for await(let X of Y.values())Q.push(X),X.kind==="directory"&&await W(X)}H(W,"collect"),await W(G);let z=new Map;z.set(".",G);for(let Y of Q){let X=(await G.resolve(Y)).join("/");z.set(X,Y)}return z},"getFsHandles"),g0=R0("AGFzbQEAAAABDANfAGAAAW9gAW8BfwMDAgECBygCE0pzdl9HZXRFcnJvcl9pbXBvcnQAAA5Kc3ZFcnJvcl9DaGVjawABChMCBwD7AQD7GwsJACAA+xr7FAAL"),w0=async function(){if(!(globalThis.navigator&&(/iPad|iPhone|iPod/.test(navigator.userAgent)||navigator.platform==="MacIntel"&&typeof navigator.maxTouchPoints<"u"&&navigator.maxTouchPoints>1)))try{let G=await WebAssembly.compile(g0);return await WebAssembly.instantiate(G)}catch(G){if(G instanceof WebAssembly.CompileError)return;throw G}}();async function Y0(){let G=await w0;if(G)return G.exports;let Q=Symbol("error marker");return{Jsv_GetError_import:H(()=>Q,"Jsv_GetError_import"),JsvError_Check:H((W)=>W===Q,"JsvError_Check")}}H(Y0,"getJsvErrorImport");function z0(G){let Q={config:G,runtimeEnv:x},W={noImageDecoding:!0,noAudioDecoding:!0,noWasmDecoding:!1,preRun:V0(G),print:G.stdout,printErr:G.stderr,onExit(z){W.exitCode=z},thisProgram:G._sysExecutable,arguments:G.args,API:Q,locateFile:H((z)=>G.indexURL+z,"locateFile"),instantiateWasm:Z0(G.indexURL)};return W}H(z0,"createSettings");function B0(G){return function(Q){let W="/";try{Q.FS.mkdirTree(G)}catch(z){console.error(`Error occurred while making a home directory '${G}':`),console.error(z),console.error(`Using '${W}' for a home directory instead`),G=W}Q.FS.chdir(G)}}H(B0,"createHomeDirectory");function H0(G){return function(Q){Object.assign(Q.ENV,G)}}H(H0,"setEnvironment");function J0(G){return G?[async(Q)=>{Q.addRunDependency("fsInitHook");try{await G(Q.FS,{sitePackages:Q.API.sitePackages})}finally{Q.removeRunDependency("fsInitHook")}}]:[]}H(J0,"callFsInitHook");function K0(G){let Q=G.HEAPU32[G._Py_Version>>>2],W=Q>>>24&255,z=Q>>>16&255,Y=Q>>>8&255;return[W,z,Y]}H(K0,"computeVersionTuple");function M0(G){let Q=t(G);return async(W)=>{W.API.pyVersionTuple=K0(W);let[z,Y]=W.API.pyVersionTuple;W.FS.mkdirTree("/lib"),W.API.sitePackages=`/lib/python${z}.${Y}/site-packages`,W.FS.mkdirTree(W.API.sitePackages),W.addRunDependency("install-stdlib");try{let X=await Q;W.FS.writeFile(`/lib/python${z}${Y}.zip`,X)}catch(X){console.error("Error occurred while installing the standard library:"),console.error(X)}finally{W.removeRunDependency("install-stdlib")}}}H(M0,"installStdlib");function V0(G){let Q;return G.stdLibURL!=null?Q=G.stdLibURL:Q=G.indexURL+"python_stdlib.zip",[M0(Q),B0(G.env.HOME),H0(G.env),X0,...J0(G.fsInit)]}H(V0,"getFileSystemInitializationFuncs");function Z0(G){if(typeof WasmOffsetConverter<"u")return;let{binary:Q,response:W}=N(G+"pyodide.asm.wasm"),z=Y0();return function(Y,X){return async function(){let{Jsv_GetError_import:B,JsvError_Check:J}=await z;Y.env.Jsv_GetError_import=B,Y.env.JsvError_Check=J;try{let K;W?K=await WebAssembly.instantiateStreaming(W,Y):K=await WebAssembly.instantiate(await Q,Y);let{instance:M,module:V}=K;X(M,V)}catch(K){console.warn("wasm instantiation failed!"),console.warn(K)}}(),{}}}H(Z0,"getInstantiateWasmFunc");var E0="314.0.1";function k(G){return G===void 0||G.endsWith("/")?G:G+"/"}H(k,"withTrailingSlash");var P=E0;async function $0(G={}){if(await y(),G.lockFileContents&&G.lockFileURL)throw Error("Can't pass both lockFileContents and lockFileURL");let Q=G.indexURL||await Q0();if(Q=k(I(Q)),G.packageBaseUrl=k(G.packageBaseUrl),G.cdnUrl=k(G.packageBaseUrl??`https://cdn.jsdelivr.net/pyodide/v${P}/full/`),!G.lockFileContents){let Y=G.lockFileURL??Q+"pyodide-lock.json";G.lockFileContents=G0(Y),G.packageBaseUrl??=W0(Y)}G.indexURL=Q,G.packageCacheDir&&(G.packageCacheDir=k(I(G.packageCacheDir)));let W={jsglobals:globalThis,stdin:globalThis.prompt?()=>globalThis.prompt():void 0,args:[],env:{},packages:[],packageCacheDir:G.packageBaseUrl,enableRunUntilComplete:!0,checkAPIVersion:!0,BUILD_ID:"2e12ea28d5e4e258977cb79dd418ccf6593712466fd333300c63fda4fc9257c2"},z=Object.assign(W,G);return z.env.HOME??="/home/pyodide",z.env.PYTHONINSPECT??="1",z}H($0,"initializeConfiguration");function j0(G){let Q=z0(G),W=Q.API;return W.lockFilePromise=Promise.resolve(G.lockFileContents),Q}H(j0,"createEmscriptenSettings");async function x0(G){if(G.createPyodideModule)return G.createPyodideModule;let Q=`${G.indexURL}pyodide.asm.mjs`;return(await b(Q)).default}H(x0,"loadWasmScript");async function C0(G,Q){if(!G._loadSnapshot)return;let W=await G._loadSnapshot,z=ArrayBuffer.isView(W)?W:new Uint8Array(W);return Q.noInitialRun=!0,Q.INITIAL_MEMORY=z.length,z}H(C0,"prepareSnapshot");async function T0(G,Q){let W=await G(Q);if(Q.exitCode!==void 0)throw new W.ExitStatus(Q.exitCode);return W}H(T0,"instantiatePyodideModule");function q0(G,Q){let W=G.API;if(Q.pyproxyToStringRepr&&W.setPyProxyToStringMethod(!0),Q.convertNullToNone&&W.setCompatNullToNone(!0),Q.toJsLiteralMap&&W.setCompatToJsLiteralMap(!0),W.version!==P&&Q.checkAPIVersion)throw Error(`Pyodide version does not match: '${P}' <==> '${W.version}'. If you updated the Pyodide version, make sure you also updated the 'indexURL' parameter passed to loadPyodide.`);G.locateFile=(z)=>{throw z.endsWith(".so")?Error(`Failed to find dynamic library "${z}"`):Error(`Unexpected call to locateFile("${z}")`)}}H(q0,"configureAPI");function D0(G,Q,W){let z=G.API,Y;return Q&&(Y=z.restoreSnapshot(Q)),z.finalizeBootstrap(Y,W._snapshotDeserializer)}H(D0,"bootstrapPyodide");async function L0(G,Q){let W=G._api;return W.sys.path.insert(0,""),W._pyodide.set_excepthook(),await W.packageIndexReady,W.initializeStreams(Q.stdin,Q.stdout,Q.stderr),G}H(L0,"finalizeSetup");async function b0(G={}){let Q=await $0(G),W=j0(Q),z=await x0(Q),Y=await C0(Q,W),X=await T0(z,W);q0(X,Q);let B=D0(X,Y,Q);return await L0(B,Q)}H(b0,"loadPyodide");export{P as version,b0 as loadPyodide}; diff --git a/sphinx_pyrepl_web/pyrepl/pyrepl.esm.js b/sphinx_pyrepl_web/pyrepl/pyrepl.esm.js index 9dca250..4b57b92 100644 --- a/sphinx_pyrepl_web/pyrepl/pyrepl.esm.js +++ b/sphinx_pyrepl_web/pyrepl/pyrepl.esm.js @@ -1,8 +1,8 @@ -import{d as On,e as Bn,f as Cn,g as Kn,i as Nn}from"./chunk-ftjk4vft.js";var K=globalThis,I=K.ShadowRoot&&(K.ShadyCSS===void 0||K.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,A=Symbol(),U=new WeakMap;class D{constructor(n,r,e){if(this._$cssResult$=!0,e!==A)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=n,this.t=r}get styleSheet(){let n=this.o,r=this.t;if(I&&n===void 0){let e=r!==void 0&&r.length===1;e&&(n=U.get(r)),n===void 0&&((this.o=n=new CSSStyleSheet).replaceSync(this.cssText),e&&U.set(r,n))}return n}toString(){return this.cssText}}var M=(n)=>new D(typeof n=="string"?n:n+"",void 0,A);var nn=(n,r)=>{if(I)n.adoptedStyleSheets=r.map((e)=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(let e of r){let l=document.createElement("style"),o=K.litNonce;o!==void 0&&l.setAttribute("nonce",o),l.textContent=e.cssText,n.appendChild(l)}},T=I?(n)=>n:(n)=>n instanceof CSSStyleSheet?((r)=>{let e="";for(let l of r.cssRules)e+=l.cssText;return M(e)})(n):n;var{is:In,defineProperty:Wn,getOwnPropertyDescriptor:Yn,getOwnPropertyNames:Xn,getOwnPropertySymbols:$n,getPrototypeOf:Dn}=Object,Y=globalThis,en=Y.trustedTypes,Tn=en?en.emptyScript:"",Vn=Y.reactiveElementPolyfillSupport,N=(n,r)=>n,W={toAttribute(n,r){switch(r){case Boolean:n=n?Tn:null;break;case Object:case Array:n=n==null?n:JSON.stringify(n)}return n},fromAttribute(n,r){let e=n;switch(r){case Boolean:e=n!==null;break;case Number:e=n===null?null:Number(n);break;case Object:case Array:try{e=JSON.parse(n)}catch(l){e=null}}return e}},V=(n,r)=>!In(n,r),rn={attribute:!0,type:String,converter:W,reflect:!1,useDefault:!1,hasChanged:V};Symbol.metadata??=Symbol("metadata"),Y.litPropertyMetadata??=new WeakMap;class m extends HTMLElement{static addInitializer(n){this._$Ei(),(this.l??=[]).push(n)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(n,r=rn){if(r.state&&(r.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(n)&&((r=Object.create(r)).wrapped=!0),this.elementProperties.set(n,r),!r.noAccessor){let e=Symbol(),l=this.getPropertyDescriptor(n,e,r);l!==void 0&&Wn(this.prototype,n,l)}}static getPropertyDescriptor(n,r,e){let{get:l,set:o}=Yn(this.prototype,n)??{get(){return this[r]},set(_){this[r]=_}};return{get:l,set(_){let s=l?.call(this);o?.call(this,_),this.requestUpdate(n,s,e)},configurable:!0,enumerable:!0}}static getPropertyOptions(n){return this.elementProperties.get(n)??rn}static _$Ei(){if(this.hasOwnProperty(N("elementProperties")))return;let n=Dn(this);n.finalize(),n.l!==void 0&&(this.l=[...n.l]),this.elementProperties=new Map(n.elementProperties)}static finalize(){if(this.hasOwnProperty(N("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(N("properties"))){let r=this.properties,e=[...Xn(r),...$n(r)];for(let l of e)this.createProperty(l,r[l])}let n=this[Symbol.metadata];if(n!==null){let r=litPropertyMetadata.get(n);if(r!==void 0)for(let[e,l]of r)this.elementProperties.set(e,l)}this._$Eh=new Map;for(let[r,e]of this.elementProperties){let l=this._$Eu(r,e);l!==void 0&&this._$Eh.set(l,r)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(n){let r=[];if(Array.isArray(n)){let e=new Set(n.flat(1/0).reverse());for(let l of e)r.unshift(T(l))}else n!==void 0&&r.push(T(n));return r}static _$Eu(n,r){let e=r.attribute;return e===!1?void 0:typeof e=="string"?e:typeof n=="string"?n.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise((n)=>this.enableUpdating=n),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach((n)=>n(this))}addController(n){(this._$EO??=new Set).add(n),this.renderRoot!==void 0&&this.isConnected&&n.hostConnected?.()}removeController(n){this._$EO?.delete(n)}_$E_(){let n=new Map,r=this.constructor.elementProperties;for(let e of r.keys())this.hasOwnProperty(e)&&(n.set(e,this[e]),delete this[e]);n.size>0&&(this._$Ep=n)}createRenderRoot(){let n=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return nn(n,this.constructor.elementStyles),n}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach((n)=>n.hostConnected?.())}enableUpdating(n){}disconnectedCallback(){this._$EO?.forEach((n)=>n.hostDisconnected?.())}attributeChangedCallback(n,r,e){this._$AK(n,e)}_$ET(n,r){let e=this.constructor.elementProperties.get(n),l=this.constructor._$Eu(n,e);if(l!==void 0&&e.reflect===!0){let o=(e.converter?.toAttribute!==void 0?e.converter:W).toAttribute(r,e.type);this._$Em=n,o==null?this.removeAttribute(l):this.setAttribute(l,o),this._$Em=null}}_$AK(n,r){let e=this.constructor,l=e._$Eh.get(n);if(l!==void 0&&this._$Em!==l){let o=e.getPropertyOptions(l),_=typeof o.converter=="function"?{fromAttribute:o.converter}:o.converter?.fromAttribute!==void 0?o.converter:W;this._$Em=l;let s=_.fromAttribute(r,o.type);this[l]=s??this._$Ej?.get(l)??s,this._$Em=null}}requestUpdate(n,r,e,l=!1,o){if(n!==void 0){let _=this.constructor;if(l===!1&&(o=this[n]),e??=_.getPropertyOptions(n),!((e.hasChanged??V)(o,r)||e.useDefault&&e.reflect&&o===this._$Ej?.get(n)&&!this.hasAttribute(_._$Eu(n,e))))return;this.C(n,r,e)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(n,r,{useDefault:e,reflect:l,wrapped:o},_){e&&!(this._$Ej??=new Map).has(n)&&(this._$Ej.set(n,_??r??this[n]),o!==!0||_!==void 0)||(this._$AL.has(n)||(this.hasUpdated||e||(r=void 0),this._$AL.set(n,r)),l===!0&&this._$Em!==n&&(this._$Eq??=new Set).add(n))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(r){Promise.reject(r)}let n=this.scheduleUpdate();return n!=null&&await n,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(let[l,o]of this._$Ep)this[l]=o;this._$Ep=void 0}let e=this.constructor.elementProperties;if(e.size>0)for(let[l,o]of e){let{wrapped:_}=o,s=this[l];_!==!0||this._$AL.has(l)||s===void 0||this.C(l,void 0,o,s)}}let n=!1,r=this._$AL;try{n=this.shouldUpdate(r),n?(this.willUpdate(r),this._$EO?.forEach((e)=>e.hostUpdate?.()),this.update(r)):this._$EM()}catch(e){throw n=!1,this._$EM(),e}n&&this._$AE(r)}willUpdate(n){}_$AE(n){this._$EO?.forEach((r)=>r.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(n)),this.updated(n)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(n){return!0}update(n){this._$Eq&&=this._$Eq.forEach((r)=>this._$ET(r,this[r])),this._$EM()}updated(n){}firstUpdated(n){}}m.elementStyles=[],m.shadowRootOptions={mode:"open"},m[N("elementProperties")]=new Map,m[N("finalized")]=new Map,Vn?.({ReactiveElement:m}),(Y.reactiveElementVersions??=[]).push("2.1.2");var Z=globalThis,ln=(n)=>n,X=Z.trustedTypes,on=X?X.createPolicy("lit-html",{createHTML:(n)=>n}):void 0;var u=`lit$${Math.random().toFixed(9).slice(2)}$`,cn="?"+u,Zn=`<${cn}>`,b=document,q=()=>b.createComment(""),G=(n)=>n===null||typeof n!="object"&&typeof n!="function",z=Array.isArray,zn=(n)=>z(n)||typeof n?.[Symbol.iterator]=="function";var F=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,_n=/-->/g,sn=/>/g,t=RegExp(`>|[ +import{d as Bn,e as Cn,f as In,g as Wn,i as qn}from"./chunk-ftjk4vft.js";var I=globalThis,W=I.ShadowRoot&&(I.ShadyCSS===void 0||I.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,M=Symbol(),U=new WeakMap;class V{constructor(n,e,r){if(this._$cssResult$=!0,r!==M)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=n,this.t=e}get styleSheet(){let n=this.o,e=this.t;if(W&&n===void 0){let r=e!==void 0&&e.length===1;r&&(n=U.get(e)),n===void 0&&((this.o=n=new CSSStyleSheet).replaceSync(this.cssText),r&&U.set(e,n))}return n}toString(){return this.cssText}}var g=(n)=>new V(typeof n=="string"?n:n+"",void 0,M);var nn=(n,e)=>{if(W)n.adoptedStyleSheets=e.map((r)=>r instanceof CSSStyleSheet?r:r.styleSheet);else for(let r of e){let l=document.createElement("style"),o=I.litNonce;o!==void 0&&l.setAttribute("nonce",o),l.textContent=r.cssText,n.appendChild(l)}},T=W?(n)=>n:(n)=>n instanceof CSSStyleSheet?((e)=>{let r="";for(let l of e.cssRules)r+=l.cssText;return g(r)})(n):n;var{is:Yn,defineProperty:Xn,getOwnPropertyDescriptor:$n,getOwnPropertyNames:Dn,getOwnPropertySymbols:Vn,getPrototypeOf:Tn}=Object,X=globalThis,en=X.trustedTypes,Zn=en?en.emptyScript:"",zn=X.reactiveElementPolyfillSupport,F=(n,e)=>n,Y={toAttribute(n,e){switch(e){case Boolean:n=n?Zn:null;break;case Object:case Array:n=n==null?n:JSON.stringify(n)}return n},fromAttribute(n,e){let r=n;switch(e){case Boolean:r=n!==null;break;case Number:r=n===null?null:Number(n);break;case Object:case Array:try{r=JSON.parse(n)}catch(l){r=null}}return r}},Z=(n,e)=>!Yn(n,e),rn={attribute:!0,type:String,converter:Y,reflect:!1,useDefault:!1,hasChanged:Z};Symbol.metadata??=Symbol("metadata"),X.litPropertyMetadata??=new WeakMap;class u extends HTMLElement{static addInitializer(n){this._$Ei(),(this.l??=[]).push(n)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(n,e=rn){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(n)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(n,e),!e.noAccessor){let r=Symbol(),l=this.getPropertyDescriptor(n,r,e);l!==void 0&&Xn(this.prototype,n,l)}}static getPropertyDescriptor(n,e,r){let{get:l,set:o}=$n(this.prototype,n)??{get(){return this[e]},set(_){this[e]=_}};return{get:l,set(_){let s=l?.call(this);o?.call(this,_),this.requestUpdate(n,s,r)},configurable:!0,enumerable:!0}}static getPropertyOptions(n){return this.elementProperties.get(n)??rn}static _$Ei(){if(this.hasOwnProperty(F("elementProperties")))return;let n=Tn(this);n.finalize(),n.l!==void 0&&(this.l=[...n.l]),this.elementProperties=new Map(n.elementProperties)}static finalize(){if(this.hasOwnProperty(F("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(F("properties"))){let e=this.properties,r=[...Dn(e),...Vn(e)];for(let l of r)this.createProperty(l,e[l])}let n=this[Symbol.metadata];if(n!==null){let e=litPropertyMetadata.get(n);if(e!==void 0)for(let[r,l]of e)this.elementProperties.set(r,l)}this._$Eh=new Map;for(let[e,r]of this.elementProperties){let l=this._$Eu(e,r);l!==void 0&&this._$Eh.set(l,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(n){let e=[];if(Array.isArray(n)){let r=new Set(n.flat(1/0).reverse());for(let l of r)e.unshift(T(l))}else n!==void 0&&e.push(T(n));return e}static _$Eu(n,e){let r=e.attribute;return r===!1?void 0:typeof r=="string"?r:typeof n=="string"?n.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise((n)=>this.enableUpdating=n),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach((n)=>n(this))}addController(n){(this._$EO??=new Set).add(n),this.renderRoot!==void 0&&this.isConnected&&n.hostConnected?.()}removeController(n){this._$EO?.delete(n)}_$E_(){let n=new Map,e=this.constructor.elementProperties;for(let r of e.keys())this.hasOwnProperty(r)&&(n.set(r,this[r]),delete this[r]);n.size>0&&(this._$Ep=n)}createRenderRoot(){let n=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return nn(n,this.constructor.elementStyles),n}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach((n)=>n.hostConnected?.())}enableUpdating(n){}disconnectedCallback(){this._$EO?.forEach((n)=>n.hostDisconnected?.())}attributeChangedCallback(n,e,r){this._$AK(n,r)}_$ET(n,e){let r=this.constructor.elementProperties.get(n),l=this.constructor._$Eu(n,r);if(l!==void 0&&r.reflect===!0){let o=(r.converter?.toAttribute!==void 0?r.converter:Y).toAttribute(e,r.type);this._$Em=n,o==null?this.removeAttribute(l):this.setAttribute(l,o),this._$Em=null}}_$AK(n,e){let r=this.constructor,l=r._$Eh.get(n);if(l!==void 0&&this._$Em!==l){let o=r.getPropertyOptions(l),_=typeof o.converter=="function"?{fromAttribute:o.converter}:o.converter?.fromAttribute!==void 0?o.converter:Y;this._$Em=l;let s=_.fromAttribute(e,o.type);this[l]=s??this._$Ej?.get(l)??s,this._$Em=null}}requestUpdate(n,e,r,l=!1,o){if(n!==void 0){let _=this.constructor;if(l===!1&&(o=this[n]),r??=_.getPropertyOptions(n),!((r.hasChanged??Z)(o,e)||r.useDefault&&r.reflect&&o===this._$Ej?.get(n)&&!this.hasAttribute(_._$Eu(n,r))))return;this.C(n,e,r)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(n,e,{useDefault:r,reflect:l,wrapped:o},_){r&&!(this._$Ej??=new Map).has(n)&&(this._$Ej.set(n,_??e??this[n]),o!==!0||_!==void 0)||(this._$AL.has(n)||(this.hasUpdated||r||(e=void 0),this._$AL.set(n,e)),l===!0&&this._$Em!==n&&(this._$Eq??=new Set).add(n))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}let n=this.scheduleUpdate();return n!=null&&await n,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(let[l,o]of this._$Ep)this[l]=o;this._$Ep=void 0}let r=this.constructor.elementProperties;if(r.size>0)for(let[l,o]of r){let{wrapped:_}=o,s=this[l];_!==!0||this._$AL.has(l)||s===void 0||this.C(l,void 0,o,s)}}let n=!1,e=this._$AL;try{n=this.shouldUpdate(e),n?(this.willUpdate(e),this._$EO?.forEach((r)=>r.hostUpdate?.()),this.update(e)):this._$EM()}catch(r){throw n=!1,this._$EM(),r}n&&this._$AE(e)}willUpdate(n){}_$AE(n){this._$EO?.forEach((e)=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(n)),this.updated(n)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(n){return!0}update(n){this._$Eq&&=this._$Eq.forEach((e)=>this._$ET(e,this[e])),this._$EM()}updated(n){}firstUpdated(n){}}u.elementStyles=[],u.shadowRootOptions={mode:"open"},u[F("elementProperties")]=new Map,u[F("finalized")]=new Map,zn?.({ReactiveElement:u}),(X.reactiveElementVersions??=[]).push("2.1.2");var z=globalThis,ln=(n)=>n,$=z.trustedTypes,on=$?$.createPolicy("lit-html",{createHTML:(n)=>n}):void 0;var a=`lit$${Math.random().toFixed(9).slice(2)}$`,xn="?"+a,Qn=`<${xn}>`,b=document,G=()=>b.createComment(""),J=(n)=>n===null||typeof n!="object"&&typeof n!="function",Q=Array.isArray,jn=(n)=>Q(n)||typeof n?.[Symbol.iterator]=="function";var q=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,_n=/-->/g,sn=/>/g,f=RegExp(`>|[ \f\r](?:([^\\s"'>=/]+)([ \f\r]*=[ \f\r]*(?:[^ -\f\r"'\`<>=]|("|')|))|$)`,"g"),pn=/'/g,yn=/"/g,an=/^(?:script|style|textarea|title)$/i,j=(n)=>(r,...e)=>({_$litType$:n,strings:r,values:e}),xn=j(1),pe=j(2),ye=j(3),h=Symbol.for("lit-noChange"),c=Symbol.for("lit-nothing"),wn=new WeakMap,f=b.createTreeWalker(b,129);function dn(n,r){if(!z(n)||!n.hasOwnProperty("raw"))throw Error("invalid template strings array");return on!==void 0?on.createHTML(r):r}var jn=(n,r)=>{let e=n.length-1,l=[],o,_=r===2?"":r===3?"":"",s=F;for(let y=0;y"?(s=o??F,a=-1):p[1]===void 0?a=-2:(a=s.lastIndex-p[2].length,i=p[1],s=p[3]===void 0?t:p[3]==='"'?yn:pn):s===yn||s===pn?s=t:s===_n||s===sn?s=F:(s=t,o=void 0);let d=s===t&&n[y+1].startsWith("/>")?" ":"";_+=s===F?w+Zn:a>=0?(l.push(i),w.slice(0,a)+"$lit$"+w.slice(a)+u+d):w+u+(a===-2?y:d)}return[dn(n,_+(n[e]||"")+(r===2?"":r===3?"":"")),l]};class J{constructor({strings:n,_$litType$:r},e){let l;this.parts=[];let o=0,_=0,s=n.length-1,y=this.parts,[w,i]=jn(n,r);if(this.el=J.createElement(w,e),f.currentNode=this.el.content,r===2||r===3){let p=this.el.content.firstChild;p.replaceWith(...p.childNodes)}for(;(l=f.nextNode())!==null&&y.length0){l.textContent=X?X.emptyScript:"";for(let x=0;x2||e[0]!==""||e[1]!==""?(this._$AH=Array(e.length-1).fill(new String),this.strings=e):this._$AH=c}_$AI(n,r=this,e,l){let o=this.strings,_=!1;if(o===void 0)n=S(this,n,r,0),_=!G(n)||n!==this._$AH&&n!==h,_&&(this._$AH=n);else{let s=n,y,w;for(n=o[0],y=0;y{let l=e?.renderBefore??r,o=l._$litPart$;if(o===void 0){let _=e?.renderBefore??null;l._$litPart$=o=new O(r.insertBefore(q(),_),_,void 0,e??{})}return o._$AI(n),o};var Q=globalThis;class v extends m{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){let n=super.createRenderRoot();return this.renderOptions.renderBefore??=n.firstChild,n}update(n){let r=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(n),this._$Do=hn(r,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return h}}v._$litElement$=!0,v.finalized=!0,Q.litElementHydrateSupport?.({LitElement:v});var gn=Q.litElementPolyfillSupport;gn?.({LitElement:v});(Q.litElementVersions??=[]).push("4.2.2");var vn=(n)=>(r,e)=>{e!==void 0?e.addInitializer(()=>{customElements.define(n,r)}):customElements.define(n,r)};var Sn=`from codeop import compile_command +\f\r"'\`<>=]|("|')|))|$)`,"g"),pn=/'/g,yn=/"/g,cn=/^(?:script|style|textarea|title)$/i,j=(n)=>(e,...r)=>({_$litType$:n,strings:e,values:r}),dn=j(1),ie=j(2),xe=j(3),h=Symbol.for("lit-noChange"),x=Symbol.for("lit-nothing"),wn=new WeakMap,t=b.createTreeWalker(b,129);function mn(n,e){if(!Q(n)||!n.hasOwnProperty("raw"))throw Error("invalid template strings array");return on!==void 0?on.createHTML(e):e}var kn=(n,e)=>{let r=n.length-1,l=[],o,_=e===2?"":e===3?"":"",s=q;for(let y=0;y"?(s=o??q,c=-1):p[1]===void 0?c=-2:(c=s.lastIndex-p[2].length,i=p[1],s=p[3]===void 0?f:p[3]==='"'?yn:pn):s===yn||s===pn?s=f:s===_n||s===sn?s=q:(s=f,o=void 0);let m=s===f&&n[y+1].startsWith("/>")?" ":"";_+=s===q?w+Qn:c>=0?(l.push(i),w.slice(0,c)+"$lit$"+w.slice(c)+a+m):w+a+(c===-2?y:m)}return[mn(n,_+(n[r]||"")+(e===2?"":e===3?"":"")),l]};class K{constructor({strings:n,_$litType$:e},r){let l;this.parts=[];let o=0,_=0,s=n.length-1,y=this.parts,[w,i]=kn(n,e);if(this.el=K.createElement(w,r),t.currentNode=this.el.content,e===2||e===3){let p=this.el.content.firstChild;p.replaceWith(...p.childNodes)}for(;(l=t.nextNode())!==null&&y.length0){l.textContent=$?$.emptyScript:"";for(let d=0;d2||r[0]!==""||r[1]!==""?(this._$AH=Array(r.length-1).fill(new String),this.strings=r):this._$AH=x}_$AI(n,e=this,r,l){let o=this.strings,_=!1;if(o===void 0)n=v(this,n,e,0),_=!J(n)||n!==this._$AH&&n!==h,_&&(this._$AH=n);else{let s=n,y,w;for(n=o[0],y=0;y{let l=r?.renderBefore??e,o=l._$litPart$;if(o===void 0){let _=r?.renderBefore??null;l._$litPart$=o=new O(e.insertBefore(G(),_),_,void 0,r??{})}return o._$AI(n),o};var k=globalThis;class S extends u{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){let n=super.createRenderRoot();return this.renderOptions.renderBefore??=n.firstChild,n}update(n){let e=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(n),this._$Do=hn(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return h}}S._$litElement$=!0,S.finalized=!0,k.litElementHydrateSupport?.({LitElement:S});var Ln=k.litElementPolyfillSupport;Ln?.({LitElement:S});(k.litElementVersions??=[]).push("4.2.2");var Sn=(n)=>(e,r)=>{r!==void 0?r.addInitializer(()=>{customElements.define(n,e)}):customElements.define(n,e)};var vn=`from codeop import compile_command import sys from _pyrepl.console import Console, Event from collections import deque @@ -588,7 +588,7 @@ async def start_repl(): browser_console.term.write("\\r\\x1b[K") # Go to start, clear line prompt = PS1 if len(lines) == 0 else PS2 browser_console.term.write(prompt + syntax_highlight(current_line)) -`;var g=null,H=null,k=Promise.resolve(),C={"catppuccin-mocha":{background:"#1e1e2e",foreground:"#cdd6f4",cursor:"#f5e0dc",cursorAccent:"#1e1e2e",selectionBackground:"#585b70",black:"#45475a",red:"#f38ba8",green:"#a6e3a1",yellow:"#f9e2af",blue:"#89b4fa",magenta:"#f5c2e7",cyan:"#94e2d5",white:"#bac2de",brightBlack:"#585b70",brightRed:"#f38ba8",brightGreen:"#a6e3a1",brightYellow:"#f9e2af",brightBlue:"#89b4fa",brightMagenta:"#f5c2e7",brightCyan:"#94e2d5",brightWhite:"#a6adc8",headerBackground:"#181825",headerForeground:"#6c7086"},"catppuccin-latte":{background:"#eff1f5",foreground:"#4c4f69",cursor:"#dc8a78",cursorAccent:"#eff1f5",selectionBackground:"#acb0be",black:"#5c5f77",red:"#d20f39",green:"#40a02b",yellow:"#df8e1d",blue:"#1e66f5",magenta:"#ea76cb",cyan:"#179299",white:"#acb0be",brightBlack:"#6c6f85",brightRed:"#d20f39",brightGreen:"#40a02b",brightYellow:"#df8e1d",brightBlue:"#1e66f5",brightMagenta:"#ea76cb",brightCyan:"#179299",brightWhite:"#bcc0cc",headerBackground:"#dce0e8",headerForeground:"#8c8fa1"}},E="catppuccin-mocha";function Fn(n){if(!n.startsWith("#"))return!0;let r=n.slice(1),e=parseInt(r.slice(0,2),16),l=parseInt(r.slice(2,4),16),o=parseInt(r.slice(4,6),16);return(0.299*e+0.587*l+0.114*o)/255<0.5}function En(n){if("black"in n&&"red"in n)return n;let r=Fn(n.background)?"catppuccin-mocha":"catppuccin-latte",e=C[r];return{cursor:e.cursor,cursorAccent:e.cursorAccent,selectionBackground:e.selectionBackground,black:e.black,red:e.red,green:e.green,yellow:e.yellow,blue:e.blue,magenta:e.magenta,cyan:e.cyan,white:e.white,brightBlack:e.brightBlack,brightRed:e.brightRed,brightGreen:e.brightGreen,brightYellow:e.brightYellow,brightBlue:e.brightBlue,brightMagenta:e.brightMagenta,brightCyan:e.brightCyan,brightWhite:e.brightWhite,background:n.background,foreground:n.foreground,headerBackground:n.headerBackground??e.headerBackground,headerForeground:n.headerForeground??e.headerForeground,promptColor:n.promptColor,pygmentsStyle:n.pygmentsStyle}}var kn={copy:'',clear:''};function Hn(n){let r,e,l=n.dataset.themeConfig;if(l)try{let s=JSON.parse(l);r=En(s),e="custom"}catch{console.warn("pyrepl-web: invalid data-theme-config JSON, falling back to default"),r=C[E],e=E}else{e=n.dataset.theme||E;let s=window.pyreplThemes?.[e]||C[e]||C[E];if(r=En(s),!window.pyreplThemes?.[e]&&!C[e])console.warn(`pyrepl-web: unknown theme "${e}", falling back to default`),e=E}let o=n.dataset.packages,_=o?o.split(",").map((s)=>s.trim()).filter(Boolean):[];return{theme:r,themeName:e,showHeader:n.dataset.header!=="false",showButtons:n.dataset.buttons!=="false",title:n.dataset.title||"python",packages:_,src:n.dataset.src||null,replaySrc:n.dataset.replaySrc||null,replayStartup:n.dataset.replay==="true",readonly:n.dataset.readonly==="true",showBanner:n.dataset.noBanner!=="true"}}async function Ln(){if(!g){let{loadPyodide:n}=await import("./chunk-e4mhg83d.js");g=n({indexURL:"https://cdn.jsdelivr.net/pyodide/v314.0.1/full/",stdout:()=>{},stderr:()=>{}})}return await g}function Rn(){if(!H)H=Promise.resolve(Sn);return H}class L{container;theme;packages;readonly;src;replaySrc;replayStartup;showHeader;showButtons;title;showBanner;constructor(n){this.container=n.container,this.theme=n.theme||E,this.packages=n.packages||[],this.readonly=n.readonly||!1,this.src=n.src,this.replaySrc=n.replaySrc,this.replayStartup=n.replayStartup||!1,this.showHeader=n.showHeader!==void 0?n.showHeader:!0,this.showButtons=n.showButtons!==void 0?n.showButtons:!0,this.title=n.title||"python",this.showBanner=n.showBanner!==void 0?n.showBanner:!0}async init(){if(this.container.dataset.theme=this.theme,this.container.dataset.packages=this.packages.join(","),this.container.dataset.readonly=this.readonly?"true":"false",this.src)this.container.dataset.src=this.src;if(this.replaySrc)this.container.dataset.replaySrc=this.replaySrc;this.container.dataset.replay=this.replayStartup?"true":"false",this.container.dataset.header=this.showHeader?"true":"false",this.container.dataset.buttons=this.showButtons?"true":"false",this.container.dataset.title=this.title,this.container.dataset.noBanner=this.showBanner?"false":"true";let{term:n,config:r}=await Gn(this.container);k=k.then(()=>Jn(this.container,n,r)),await k}}function Un(){if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",Pn);else Pn()}function An(){if(document.getElementById("pyrepl-styles"))return;let n=document.createElement("link");n.rel="stylesheet",n.href="https://cdn.jsdelivr.net/npm/@xterm/xterm/css/xterm.css",document.head.appendChild(n);let r=document.createElement("style");r.id="pyrepl-styles",r.textContent=` +`;var Rn=/^(https?:|emfs:)/i;function An(n,e=document.baseURI){if(Rn.test(n))return n;if(n.includes(" @ "))return n;if(!n.includes("/")&&!n.endsWith(".whl"))return n;return new URL(n,e).href}function En(n,e=document.baseURI){return n.map((r)=>An(r,e))}var H=null,L=null,N=Promise.resolve(),C={"catppuccin-mocha":{background:"#1e1e2e",foreground:"#cdd6f4",cursor:"#f5e0dc",cursorAccent:"#1e1e2e",selectionBackground:"#585b70",black:"#45475a",red:"#f38ba8",green:"#a6e3a1",yellow:"#f9e2af",blue:"#89b4fa",magenta:"#f5c2e7",cyan:"#94e2d5",white:"#bac2de",brightBlack:"#585b70",brightRed:"#f38ba8",brightGreen:"#a6e3a1",brightYellow:"#f9e2af",brightBlue:"#89b4fa",brightMagenta:"#f5c2e7",brightCyan:"#94e2d5",brightWhite:"#a6adc8",headerBackground:"#181825",headerForeground:"#6c7086"},"catppuccin-latte":{background:"#eff1f5",foreground:"#4c4f69",cursor:"#dc8a78",cursorAccent:"#eff1f5",selectionBackground:"#acb0be",black:"#5c5f77",red:"#d20f39",green:"#40a02b",yellow:"#df8e1d",blue:"#1e66f5",magenta:"#ea76cb",cyan:"#179299",white:"#acb0be",brightBlack:"#6c6f85",brightRed:"#d20f39",brightGreen:"#40a02b",brightYellow:"#df8e1d",brightBlue:"#1e66f5",brightMagenta:"#ea76cb",brightCyan:"#179299",brightWhite:"#bcc0cc",headerBackground:"#dce0e8",headerForeground:"#8c8fa1"}},E="catppuccin-mocha";function Gn(n){if(!n.startsWith("#"))return!0;let e=n.slice(1),r=parseInt(e.slice(0,2),16),l=parseInt(e.slice(2,4),16),o=parseInt(e.slice(4,6),16);return(0.299*r+0.587*l+0.114*o)/255<0.5}function Nn(n){if("black"in n&&"red"in n)return n;let e=Gn(n.background)?"catppuccin-mocha":"catppuccin-latte",r=C[e];return{cursor:r.cursor,cursorAccent:r.cursorAccent,selectionBackground:r.selectionBackground,black:r.black,red:r.red,green:r.green,yellow:r.yellow,blue:r.blue,magenta:r.magenta,cyan:r.cyan,white:r.white,brightBlack:r.brightBlack,brightRed:r.brightRed,brightGreen:r.brightGreen,brightYellow:r.brightYellow,brightBlue:r.brightBlue,brightMagenta:r.brightMagenta,brightCyan:r.brightCyan,brightWhite:r.brightWhite,background:n.background,foreground:n.foreground,headerBackground:n.headerBackground??r.headerBackground,headerForeground:n.headerForeground??r.headerForeground,promptColor:n.promptColor,pygmentsStyle:n.pygmentsStyle}}var Pn={copy:'',clear:''};function Un(n){let e,r,l=n.dataset.themeConfig;if(l)try{let s=JSON.parse(l);e=Nn(s),r="custom"}catch{console.warn("pyrepl-web: invalid data-theme-config JSON, falling back to default"),e=C[E],r=E}else{r=n.dataset.theme||E;let s=window.pyreplThemes?.[r]||C[r]||C[E];if(e=Nn(s),!window.pyreplThemes?.[r]&&!C[r])console.warn(`pyrepl-web: unknown theme "${r}", falling back to default`),r=E}let o=n.dataset.packages,_=o?o.split(",").map((s)=>s.trim()).filter(Boolean):[];return{theme:e,themeName:r,showHeader:n.dataset.header!=="false",showButtons:n.dataset.buttons!=="false",title:n.dataset.title||"python",packages:_,src:n.dataset.src||null,replaySrc:n.dataset.replaySrc||null,replayStartup:n.dataset.replay==="true",readonly:n.dataset.readonly==="true",showBanner:n.dataset.noBanner!=="true"}}async function Mn(){if(!H){let{loadPyodide:n}=await import("./chunk-cnc6ympw.js");H=n({indexURL:"https://cdn.jsdelivr.net/pyodide/v314.0.1/full/",stdout:()=>{},stderr:()=>{}})}return await H}function gn(){if(!L)L=Promise.resolve(vn);return L}class R{container;theme;packages;readonly;src;replaySrc;replayStartup;showHeader;showButtons;title;showBanner;constructor(n){this.container=n.container,this.theme=n.theme||E,this.packages=n.packages||[],this.readonly=n.readonly||!1,this.src=n.src,this.replaySrc=n.replaySrc,this.replayStartup=n.replayStartup||!1,this.showHeader=n.showHeader!==void 0?n.showHeader:!0,this.showButtons=n.showButtons!==void 0?n.showButtons:!0,this.title=n.title||"python",this.showBanner=n.showBanner!==void 0?n.showBanner:!0}async init(){if(this.container.dataset.theme=this.theme,this.container.dataset.packages=this.packages.join(","),this.container.dataset.readonly=this.readonly?"true":"false",this.src)this.container.dataset.src=this.src;if(this.replaySrc)this.container.dataset.replaySrc=this.replaySrc;this.container.dataset.replay=this.replayStartup?"true":"false",this.container.dataset.header=this.showHeader?"true":"false",this.container.dataset.buttons=this.showButtons?"true":"false",this.container.dataset.title=this.title,this.container.dataset.noBanner=this.showBanner?"false":"true";let{term:n,config:e}=await Kn(this.container);N=N.then(()=>On(this.container,n,e)),await N}}function ne(){if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",Fn);else Fn()}function ee(){if(document.getElementById("pyrepl-styles"))return;let n=document.createElement("link");n.rel="stylesheet",n.href="https://cdn.jsdelivr.net/npm/@xterm/xterm/css/xterm.css",document.head.appendChild(n);let e=document.createElement("style");e.id="pyrepl-styles",e.textContent=` .pyrepl { display: inline-block; position: relative; @@ -682,16 +682,16 @@ async def start_repl(): scrollbar-width: none; background-color: var(--pyrepl-bg) !important; } - `,document.head.appendChild(r)}function Mn(n,r){let e=r.headerBackground||r.black,l=r.headerForeground||r.brightBlack;n.style.setProperty("--pyrepl-bg",r.background),n.style.setProperty("--pyrepl-header-bg",e),n.style.setProperty("--pyrepl-header-title",l),n.style.setProperty("--pyrepl-red",r.red),n.style.setProperty("--pyrepl-yellow",r.yellow),n.style.setProperty("--pyrepl-green",r.green),n.style.setProperty("--pyrepl-shadow","0 4px 24px rgba(0, 0, 0, 0.3)")}function qn(){let n=document.createElement("div");return n.className="pyrepl-header-buttons",n.innerHTML=` - - - `,n}function ne(n){let r=document.createElement("div");r.className="pyrepl-header";let e=document.createElement("div");e.className="pyrepl-header-dots",e.innerHTML=` + `,document.head.appendChild(e)}function re(n,e){let r=e.headerBackground||e.black,l=e.headerForeground||e.brightBlack;n.style.setProperty("--pyrepl-bg",e.background),n.style.setProperty("--pyrepl-header-bg",r),n.style.setProperty("--pyrepl-header-title",l),n.style.setProperty("--pyrepl-red",e.red),n.style.setProperty("--pyrepl-yellow",e.yellow),n.style.setProperty("--pyrepl-green",e.green),n.style.setProperty("--pyrepl-shadow","0 4px 24px rgba(0, 0, 0, 0.3)")}function Jn(){let n=document.createElement("div");return n.className="pyrepl-header-buttons",n.innerHTML=` + + + `,n}function le(n){let e=document.createElement("div");e.className="pyrepl-header";let r=document.createElement("div");r.className="pyrepl-header-dots",r.innerHTML=`
- `;let l=document.createElement("div");if(l.className="pyrepl-header-title",l.textContent=n.title,r.appendChild(e),r.appendChild(l),n.showButtons)r.appendChild(qn());else{let o=document.createElement("div");o.style.width="48px",r.appendChild(o)}return r}async function Gn(n){An();let r=Hn(n);if(Mn(n,r.theme),r.showHeader)n.appendChild(ne(r));else if(r.showButtons){let _=qn();_.classList.add("pyrepl-floating-buttons"),n.classList.add("pyrepl--floating-buttons"),n.appendChild(_)}let e=await import("./chunk-jfxv8vy0.js"),l=document.createElement("div");n.appendChild(l);let o=new e.Terminal({cursorBlink:!r.readonly,cursorStyle:r.readonly?"bar":"block",fontSize:14,fontFamily:"monospace",theme:r.theme,disableStdin:r.readonly});return o.open(l),{term:o,config:r}}async function Jn(n,r,e){let l=await Ln();if(await l.loadPackage("micropip"),e.packages.length>0)await l.pyimport("micropip").install(e.packages);let o=e.packages.length>0?` (installed packages: ${e.packages.join(", ")})`:"",s=`${await l.runPythonAsync("import sys; sys.version.split()[0]")}${o}`;if(e.showBanner)r.write(`\x1B[90m${s}\x1B[0m\r -`);if(globalThis.term=r,globalThis.pyreplTheme=e.themeName,globalThis.pyreplPygmentsFallback=Fn(e.theme.background)?"catppuccin-mocha":"catppuccin-latte",globalThis.pyreplInfo=s,globalThis.pyreplReadonly=e.readonly,globalThis.pyreplPromptColor=e.theme.promptColor||"green",globalThis.pyreplPygmentsStyle=e.theme.pygmentsStyle,globalThis.pyreplBanner=e.showBanner,e.src)try{let i=await fetch(e.src);if(i.ok)globalThis.pyreplStartupScript=await i.text();else console.warn(`pyrepl-web: failed to fetch script from ${e.src}`)}catch(i){console.warn(`pyrepl-web: error fetching script from ${e.src}`,i)}if(e.replaySrc)try{let i=await fetch(e.replaySrc);if(i.ok)globalThis.pyreplReplayScript=await i.text();else{let p=`pyrepl-web: failed to fetch replay script from ${e.replaySrc}`;console.warn(p),r.write(`\x1B[31m${p}\x1B[0m\r -`)}}catch(i){let p=`pyrepl-web: error fetching replay script from ${e.replaySrc}`;console.warn(p,i),r.write(`\x1B[31m${p}\x1B[0m\r -`)}globalThis.pyreplReplayStartup=e.replayStartup;let y=await Rn();l.runPython(y),l.runPythonAsync("await start_repl()");while(!globalThis.currentBrowserConsole)await new Promise((i)=>setTimeout(i,10));let w=globalThis.currentBrowserConsole;if(globalThis.currentBrowserConsole=null,globalThis.term=null,globalThis.pyreplStartupScript=void 0,globalThis.pyreplReplayScript=void 0,globalThis.pyreplReplayStartup=!1,globalThis.pyreplTheme="",globalThis.pyreplPygmentsFallback="",globalThis.pyreplInfo="",globalThis.pyreplReadonly=!1,globalThis.pyreplPromptColor="",globalThis.pyreplPygmentsStyle=void 0,globalThis.pyreplBanner=!1,!e.readonly)r.onData((i)=>{for(let p of i)w.push_char(p.charCodeAt(0))});if(e.showButtons){let i=n.querySelector('[data-action="copy"]'),p=n.querySelector('[data-action="clear"]');i?.addEventListener("click",()=>{let a=r.buffer.active,x="";for(let d=0;d{if(r.reset(),e.showBanner)r.write(`\x1B[90m${s}\x1B[0m\r -`);r.write("\x1B[32m>>> \x1B[0m")})}}async function Pn(){let n=document.querySelectorAll(".pyrepl"),r=Array.from(n).filter((l)=>!l.closest("py-repl")&&!l.dataset.pyreplInitialized);if(r.length===0)return;for(let l of r)l.dataset.pyreplInitialized="true";let e=await Promise.all(Array.from(r).map(async(l)=>({container:l,...await Gn(l)})));for(let{container:l,term:o,config:_}of e)k=k.then(()=>Jn(l,o,_));await k}Un();var lr=[vn("py-repl")],rr=v,ee=On(rr);class P extends rr{static properties={theme:{type:String},packages:{type:String},replTitle:{type:String,attribute:"repl-title"},noBanner:{type:Boolean,attribute:"no-banner"},isReadonly:{type:Boolean,attribute:"readonly"},noButtons:{type:Boolean,attribute:"no-buttons"},noHeader:{type:Boolean,attribute:"no-header"},src:{type:String},replaySrc:{type:String,attribute:"replay-src"},replayStartup:{type:Boolean,attribute:"replay"}};constructor(){super();this.theme="catppuccin-mocha",this.packages="",this.replTitle="Python REPL",this.noBanner=!1,this.isReadonly=!1,this.noButtons=!1,this.noHeader=!1,this.src="",this.replaySrc="",this.replayStartup=!1}createRenderRoot(){return this}firstUpdated(n){super.firstUpdated(n);let r=this.querySelector(".pyrepl");if(!r){console.error("pyrepl-web: .pyrepl container not found in ");return}r.dataset.pyreplInitialized="true",new L({container:r,theme:this.theme,packages:this.packages.split(",").map((l)=>l.trim()).filter((l)=>l.length>0),readonly:this.isReadonly,src:this.src||void 0,replaySrc:this.replaySrc||void 0,replayStartup:this.replayStartup,showHeader:!this.noHeader,showButtons:!this.noButtons,title:this.replTitle,showBanner:!this.noBanner}).init()}render(){return xn`
`}}P=Kn(ee,0,"PyRepl",lr,P),Cn(ee,1,P),Bn(ee,P);let _PyRepl=P;export{P as PyRepl}; + `;let l=document.createElement("div");if(l.className="pyrepl-header-title",l.textContent=n.title,e.appendChild(r),e.appendChild(l),n.showButtons)e.appendChild(Jn());else{let o=document.createElement("div");o.style.width="48px",e.appendChild(o)}return e}async function Kn(n){ee();let e=Un(n);if(re(n,e.theme),e.showHeader)n.appendChild(le(e));else if(e.showButtons){let _=Jn();_.classList.add("pyrepl-floating-buttons"),n.classList.add("pyrepl--floating-buttons"),n.appendChild(_)}let r=await import("./chunk-jfxv8vy0.js"),l=document.createElement("div");n.appendChild(l);let o=new r.Terminal({cursorBlink:!e.readonly,cursorStyle:e.readonly?"bar":"block",fontSize:14,fontFamily:"monospace",theme:e.theme,disableStdin:e.readonly});return o.open(l),{term:o,config:e}}async function On(n,e,r){let l=await Mn();if(await l.loadPackage("micropip"),r.packages.length>0)await l.pyimport("micropip").install(En(r.packages));let o=r.packages.length>0?` (installed packages: ${r.packages.join(", ")})`:"",s=`${await l.runPythonAsync("import sys; sys.version.split()[0]")}${o}`;if(r.showBanner)e.write(`\x1B[90m${s}\x1B[0m\r +`);if(globalThis.term=e,globalThis.pyreplTheme=r.themeName,globalThis.pyreplPygmentsFallback=Gn(r.theme.background)?"catppuccin-mocha":"catppuccin-latte",globalThis.pyreplInfo=s,globalThis.pyreplReadonly=r.readonly,globalThis.pyreplPromptColor=r.theme.promptColor||"green",globalThis.pyreplPygmentsStyle=r.theme.pygmentsStyle,globalThis.pyreplBanner=r.showBanner,r.src)try{let i=await fetch(r.src);if(i.ok)globalThis.pyreplStartupScript=await i.text();else console.warn(`pyrepl-web: failed to fetch script from ${r.src}`)}catch(i){console.warn(`pyrepl-web: error fetching script from ${r.src}`,i)}if(r.replaySrc)try{let i=await fetch(r.replaySrc);if(i.ok)globalThis.pyreplReplayScript=await i.text();else{let p=`pyrepl-web: failed to fetch replay script from ${r.replaySrc}`;console.warn(p),e.write(`\x1B[31m${p}\x1B[0m\r +`)}}catch(i){let p=`pyrepl-web: error fetching replay script from ${r.replaySrc}`;console.warn(p,i),e.write(`\x1B[31m${p}\x1B[0m\r +`)}globalThis.pyreplReplayStartup=r.replayStartup;let y=await gn();l.runPython(y),l.runPythonAsync("await start_repl()");while(!globalThis.currentBrowserConsole)await new Promise((i)=>setTimeout(i,10));let w=globalThis.currentBrowserConsole;if(globalThis.currentBrowserConsole=null,globalThis.term=null,globalThis.pyreplStartupScript=void 0,globalThis.pyreplReplayScript=void 0,globalThis.pyreplReplayStartup=!1,globalThis.pyreplTheme="",globalThis.pyreplPygmentsFallback="",globalThis.pyreplInfo="",globalThis.pyreplReadonly=!1,globalThis.pyreplPromptColor="",globalThis.pyreplPygmentsStyle=void 0,globalThis.pyreplBanner=!1,!r.readonly)e.onData((i)=>{for(let p of i)w.push_char(p.charCodeAt(0))});if(r.showButtons){let i=n.querySelector('[data-action="copy"]'),p=n.querySelector('[data-action="clear"]');i?.addEventListener("click",()=>{let c=e.buffer.active,d="";for(let m=0;m{if(e.reset(),r.showBanner)e.write(`\x1B[90m${s}\x1B[0m\r +`);e.write("\x1B[32m>>> \x1B[0m")})}}async function Fn(){let n=document.querySelectorAll(".pyrepl"),e=Array.from(n).filter((l)=>!l.closest("py-repl")&&!l.dataset.pyreplInitialized);if(e.length===0)return;for(let l of e)l.dataset.pyreplInitialized="true";let r=await Promise.all(Array.from(e).map(async(l)=>({container:l,...await Kn(l)})));for(let{container:l,term:o,config:_}of r)N=N.then(()=>On(l,o,_));await N}ne();var yr=[Sn("py-repl")],pr=S,oe=Bn(pr);class P extends pr{static properties={theme:{type:String},packages:{type:String},replTitle:{type:String,attribute:"repl-title"},noBanner:{type:Boolean,attribute:"no-banner"},isReadonly:{type:Boolean,attribute:"readonly"},noButtons:{type:Boolean,attribute:"no-buttons"},noHeader:{type:Boolean,attribute:"no-header"},src:{type:String},replaySrc:{type:String,attribute:"replay-src"},replayStartup:{type:Boolean,attribute:"replay"}};constructor(){super();this.theme="catppuccin-mocha",this.packages="",this.replTitle="Python REPL",this.noBanner=!1,this.isReadonly=!1,this.noButtons=!1,this.noHeader=!1,this.src="",this.replaySrc="",this.replayStartup=!1}createRenderRoot(){return this}firstUpdated(n){super.firstUpdated(n);let e=this.querySelector(".pyrepl");if(!e){console.error("pyrepl-web: .pyrepl container not found in ");return}e.dataset.pyreplInitialized="true",new R({container:e,theme:this.theme,packages:this.packages.split(",").map((l)=>l.trim()).filter((l)=>l.length>0),readonly:this.isReadonly,src:this.src||void 0,replaySrc:this.replaySrc||void 0,replayStartup:this.replayStartup,showHeader:!this.noHeader,showButtons:!this.noButtons,title:this.replTitle,showBanner:!this.noBanner}).init()}render(){return dn`
`}}P=Wn(oe,0,"PyRepl",yr,P),In(oe,1,P),Cn(oe,P);let _PyRepl=P;export{P as PyRepl}; diff --git a/sphinx_pyrepl_web/pyrepl/version.txt b/sphinx_pyrepl_web/pyrepl/version.txt index 9531d95..5e915e6 100644 --- a/sphinx_pyrepl_web/pyrepl/version.txt +++ b/sphinx_pyrepl_web/pyrepl/version.txt @@ -1 +1 @@ -cursor/repl-startup-replay-2e3f@5299fa92dc9aada6ae0fe757ea93bee824088a15 +cursor/resolve-package-urls-0bbd@1d95c619c6251475f41ea20eec631c45903b1498 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,42 @@ 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_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/pyrepl_test_pkg/pyproject.toml b/tests/fixtures/pyrepl_test_pkg/pyproject.toml new file mode 100644 index 0000000..f9f3e3f --- /dev/null +++ b/tests/fixtures/pyrepl_test_pkg/pyproject.toml @@ -0,0 +1,8 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "pyrepl_test_pkg" +version = "1.0.0" +description = "Minimal package for sphinx-pyrepl-web wheel fixture tests" diff --git a/tests/fixtures/pyrepl_test_pkg/pyrepl_test_pkg/__init__.py b/tests/fixtures/pyrepl_test_pkg/pyrepl_test_pkg/__init__.py new file mode 100644 index 0000000..39d39ca --- /dev/null +++ b/tests/fixtures/pyrepl_test_pkg/pyrepl_test_pkg/__init__.py @@ -0,0 +1,5 @@ +"""Tiny package used as a committed wheel fixture for local wheel tests.""" + + +def ping() -> str: + return "pong" 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/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/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 new file mode 100644 index 0000000..d8a2685 Binary files /dev/null and b/tests/fixtures/wheels/pyrepl_test_pkg-1.0.0-py3-none-any.whl differ 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 66% rename from tests/test_autodoc_include.py rename to tests/integration/test_include.py index 01464ce..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,30 +66,14 @@ 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 '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() 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_autodoc_bootstrap.py b/tests/test_autodoc_bootstrap.py deleted file mode 100644 index 2f2d473..0000000 --- a/tests/test_autodoc_bootstrap.py +++ /dev/null @@ -1,187 +0,0 @@ -import json -import logging -import sys -from pathlib import Path -from unittest.mock import MagicMock - -from sphinx.application import Sphinx - -from sphinx_pyrepl_web import _resolve_autodoc_bootstrap - -ROOT = Path(__file__).resolve().parents[1] -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. - - Examples: - >>> greet() - 'hi' - - """ - return "hi" -'''.strip() - + "\n", - encoding="utf-8", - ) - outdir = tmp_path / "_build" - doctreedir = tmp_path / "_doctree" - - (srcdir / "conf.py").write_text( - """ -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parent / "_static")) -extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_pyrepl_web"] -master_doc = "index" -pyrepl_js = "pyrepl.js" -pyrepl_doctest_blocks = "autodoc" -""", - encoding="utf-8", - ) - (srcdir / "index.rst").write_text(".. autofunction:: demo.greet\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() - - html = (outdir / "index.html").read_text(encoding="utf-8") - assert 'src="_static/demo.py"' in html - assert 'replay-src="_static/pyrepl/index-1.py"' in html - assert "pyrepl.js" in html - assert (outdir / "_static" / "demo.py").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 list(replay_files) == ["index-1.py"] - - -def test_autodoc_bootstrap_uses_packages_for_installed_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): - """A demo widget. - - Example: - >>> w = Widget() - >>> w.label - 'ready' - - """ - pass -'''.strip() - + "\n", - encoding="utf-8", - ) - (pkg_dir / "core.py").write_text( - ''' -class Widget: - 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") - - 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() - - html = (outdir / "index.html").read_text(encoding="utf-8") - assert 'packages="installed_pkg"' in html - assert 'replay-src="_static/pyrepl/index-1.py"' in html - assert "", html.index("")] - 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") - - app = MagicMock() - app.config.pyrepl_autodoc_bootstrap = True - - env = MagicMock() - env.srcdir = "/tmp/docs" - env.metadata = {"index": {}} - env.note_dependency = MagicMock() - - sig = MagicMock() - sig.get.side_effect = lambda key, default=None: { - "module": "nonexistent_bootstrap_mod_xyz", - "fullname": "missing", - }.get(key, default) - - 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 - ) diff --git a/tests/test_autodoc_doctest.py b/tests/test_autodoc_doctest.py deleted file mode 100644 index 42556ca..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="repl_test_demo"' 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/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