sphinx-pyrepl-web 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/PKG-INFO +43 -17
  2. sphinx_pyrepl_web-0.2.0/README.md +113 -0
  3. sphinx_pyrepl_web-0.2.0/docs/_static/autodoc_demo.py +12 -0
  4. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/docs/conf.py +7 -0
  5. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/docs/example.rst +47 -10
  6. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/pyproject.toml +5 -0
  7. sphinx_pyrepl_web-0.2.0/sphinx_pyrepl_web/__init__.py +316 -0
  8. sphinx_pyrepl_web-0.2.0/tests/test_autodoc_bootstrap.py +187 -0
  9. sphinx_pyrepl_web-0.2.0/tests/test_autodoc_doctest.py +158 -0
  10. sphinx_pyrepl_web-0.2.0/tests/test_autodoc_include.py +100 -0
  11. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/tests/test_build_replay.py +48 -0
  12. sphinx_pyrepl_web-0.2.0/tests/test_doctest_to_replay_source.py +55 -0
  13. sphinx_pyrepl_web-0.1.0/README.md +0 -87
  14. sphinx_pyrepl_web-0.1.0/sphinx_pyrepl_web/__init__.py +0 -180
  15. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/.github/workflows/python-package.yml +0 -0
  16. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/.gitignore +0 -0
  17. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/.readthedocs.yml +0 -0
  18. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/LICENSE +0 -0
  19. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/docs/_static/replay_demo.py +0 -0
  20. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/docs/_static/setup.py +0 -0
  21. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/docs/index.rst +0 -0
  22. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/scripts/vendor_repl.py +0 -0
  23. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/sphinx_pyrepl_web/pyrepl/chunk-94xkfg72.js +0 -0
  24. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/sphinx_pyrepl_web/pyrepl/chunk-e4mhg83d.js +0 -0
  25. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/sphinx_pyrepl_web/pyrepl/chunk-ftjk4vft.js +0 -0
  26. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/sphinx_pyrepl_web/pyrepl/chunk-jfxv8vy0.js +0 -0
  27. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/sphinx_pyrepl_web/pyrepl/chunk-vx91qfkd.js +0 -0
  28. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/sphinx_pyrepl_web/pyrepl/chunk-x20ze186.js +0 -0
  29. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/sphinx_pyrepl_web/pyrepl/pyrepl.esm.js +0 -0
  30. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/sphinx_pyrepl_web/pyrepl/pyrepl.js +0 -0
  31. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/sphinx_pyrepl_web/pyrepl/version.txt +0 -0
  32. {sphinx_pyrepl_web-0.1.0 → sphinx_pyrepl_web-0.2.0}/tests/test_basic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sphinx-pyrepl-web
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A Sphinx extension for embedding pyrepl-web Python REPLs in documentation.
5
5
  Keywords: sphinx,pyrepl,pyodide,repl
6
6
  Author-email: Christian López Barrón <chris.gfz@gmail.com>
@@ -74,30 +74,56 @@ Embed a REPL with the `py-repl` directive:
74
74
 
75
75
  ### Directive options
76
76
 
77
- Most options drive [pyrepl-web](https://github.com/chrizzFTD/pyrepl-web)'s attributes, with a few exceptions unique to this extension:
77
+ All options drive [pyrepl-web](https://github.com/chrizzFTD/pyrepl-web)'s attributes, with the exception of `silent`:
78
78
 
79
- | Option | Description | `pyrepl-web` attr? |
80
- |--------|-------------|------------------|
81
- | `:theme:` | Color theme (`catppuccin-mocha`, `catppuccin-latte`) | ✅ |
82
- | `:packages:` | Comma-separated PyPI packages to preload | ✅ |
83
- | `:repl-title:` | Title in the REPL header | ✅ |
84
- | `:src:` | Path to a Python startup script | ✅ |
85
- | `:replay:` | Replay `:src:` with interactive prompts instead of silent load | ✅ |
86
- | `:silent:` | Keep `:src:` silent even when combined with a directive body | ❌ |
87
- | `:strip-prompts:` | Strip ``>>>`` / ``...`` prefixes from directive body | ❌ |
88
- | `:no-header:` | Hide the header bar | ✅ |
89
- | `:no-buttons:` | Hide copy/clear buttons | ✅ |
90
- | `:readonly:` | Disable input | |
91
- | `:no-banner:` | Hide the Python version banner | ✅ |
79
+ | Option | Description |
80
+ |--------|-------------|
81
+ | `:theme:` | Color theme (`catppuccin-mocha`, `catppuccin-latte`) |
82
+ | `:packages:` | Comma-separated PyPI packages to preload |
83
+ | `:repl-title:` | Title in the REPL header |
84
+ | `:src:` | Path to a Python startup script |
85
+ | `:replay:` | Replay `:src:` with interactive prompts instead of silent load |
86
+ | `:silent:` | Keep `:src:` silent even when combined with a directive body |
87
+ | `:no-header:` | Hide the header bar |
88
+ | `:no-buttons:` | Hide copy/clear buttons |
89
+ | `:readonly:` | Disable input |
90
+ | `:no-banner:` | Hide the Python version banner |
92
91
 
93
- Directive body content (inline Python in the `.. py-repl::` block) is also extension-only: it is written to `_static/pyrepl/` at build time and emitted as `replay-src`.
92
+ Python code within the `.. py-repl::` directive is written to `_static/pyrepl/` at build time and emitted as `replay-src`.
94
93
 
95
94
  Optional Sphinx config:
96
95
 
97
96
  ```python
98
97
  pyrepl_js = "../pyrepl.js" # default; path to the pyrepl-web loader script
98
+ pyrepl_doctest_blocks = False # default; see Docstring conversion below
99
+ pyrepl_autodoc_bootstrap = True # default; silent :src: bootstrap for autodoc REPLs
99
100
  ```
100
101
 
102
+ ### Docstring conversion
103
+
104
+ Converting doctest examples from docstrings into interactive REPLs is opt-in with `sphinx.ext.autodoc`:
105
+
106
+ ```python
107
+ # conf.py
108
+ extensions = [
109
+ "sphinx.ext.autodoc",
110
+ "sphinx_pyrepl_web",
111
+ ]
112
+ pyrepl_doctest_blocks = "autodoc"
113
+ ```
114
+
115
+ | | `pyrepl_doctest_blocks` options |
116
+ |-------------------|-------------------------------------|
117
+ | `False` (default) | Disable autodoc conversion |
118
+ | `"autodoc"` | Convert doctests found by autodoc |
119
+ | `"all"` | Transform every doctest block found |
120
+
121
+
122
+ | | `pyrepl_autodoc_bootstrap` options |
123
+ |------------------|------------------------------------------------------------------------------|
124
+ | `True` (default) | Bootstrap REPL: in-tree modules via silent `:src:`, packages via `packages=` |
125
+ | `False` | Replay doctest input only; documented names are not pre-defined |
126
+
101
127
  ## Updating pyrepl-web
102
128
 
103
129
  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:
@@ -109,7 +135,7 @@ python scripts/vendor_repl.py
109
135
  The `grill` branch is used by default. Use the `branch` argument to specify a different one:
110
136
 
111
137
  ```bash
112
- python scripts/vendor_repl.py --branch cursor/repl-startup-replay-2e3f
138
+ python scripts/vendor_repl.py --branch custom/feature-branch
113
139
  ```
114
140
 
115
141
  This requires [git](https://git-scm.com/) and [Bun](https://bun.sh/).
@@ -0,0 +1,113 @@
1
+ # sphinx-pyrepl-web
2
+
3
+ Sphinx extension to embed [pyrepl-web](https://github.com/chrizzFTD/pyrepl-web) in documentation.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install sphinx-pyrepl-web
9
+ ```
10
+
11
+ For development:
12
+
13
+ ```bash
14
+ pip install -e ".[test,docs]"
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ Add the extension to the target project's `conf.py`:
20
+
21
+ ```python
22
+ extensions = [
23
+ "sphinx_pyrepl_web",
24
+ ]
25
+ ```
26
+
27
+ Embed a REPL with the `py-repl` directive:
28
+
29
+ ```rst
30
+ .. py-repl::
31
+
32
+ .. py-repl::
33
+ :theme: catppuccin-latte
34
+ :no-header:
35
+
36
+ .. py-repl::
37
+ :src: setup.py
38
+ :packages: numpy
39
+
40
+ .. py-repl::
41
+ :no-header:
42
+
43
+ >>> import math
44
+ >>> math.sqrt(16)
45
+ ```
46
+
47
+ ### Directive options
48
+
49
+ All options drive [pyrepl-web](https://github.com/chrizzFTD/pyrepl-web)'s attributes, with the exception of `silent`:
50
+
51
+ | Option | Description |
52
+ |--------|-------------|
53
+ | `:theme:` | Color theme (`catppuccin-mocha`, `catppuccin-latte`) |
54
+ | `:packages:` | Comma-separated PyPI packages to preload |
55
+ | `:repl-title:` | Title in the REPL header |
56
+ | `:src:` | Path to a Python startup script |
57
+ | `:replay:` | Replay `:src:` with interactive prompts instead of silent load |
58
+ | `:silent:` | Keep `:src:` silent even when combined with a directive body |
59
+ | `:no-header:` | Hide the header bar |
60
+ | `:no-buttons:` | Hide copy/clear buttons |
61
+ | `:readonly:` | Disable input |
62
+ | `:no-banner:` | Hide the Python version banner |
63
+
64
+ Python code within the `.. py-repl::` directive is written to `_static/pyrepl/` at build time and emitted as `replay-src`.
65
+
66
+ Optional Sphinx config:
67
+
68
+ ```python
69
+ pyrepl_js = "../pyrepl.js" # default; path to the pyrepl-web loader script
70
+ pyrepl_doctest_blocks = False # default; see Docstring conversion below
71
+ pyrepl_autodoc_bootstrap = True # default; silent :src: bootstrap for autodoc REPLs
72
+ ```
73
+
74
+ ### Docstring conversion
75
+
76
+ Converting doctest examples from docstrings into interactive REPLs is opt-in with `sphinx.ext.autodoc`:
77
+
78
+ ```python
79
+ # conf.py
80
+ extensions = [
81
+ "sphinx.ext.autodoc",
82
+ "sphinx_pyrepl_web",
83
+ ]
84
+ pyrepl_doctest_blocks = "autodoc"
85
+ ```
86
+
87
+ | | `pyrepl_doctest_blocks` options |
88
+ |-------------------|-------------------------------------|
89
+ | `False` (default) | Disable autodoc conversion |
90
+ | `"autodoc"` | Convert doctests found by autodoc |
91
+ | `"all"` | Transform every doctest block found |
92
+
93
+
94
+ | | `pyrepl_autodoc_bootstrap` options |
95
+ |------------------|------------------------------------------------------------------------------|
96
+ | `True` (default) | Bootstrap REPL: in-tree modules via silent `:src:`, packages via `packages=` |
97
+ | `False` | Replay doctest input only; documented names are not pre-defined |
98
+
99
+ ## Updating pyrepl-web
100
+
101
+ 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:
102
+
103
+ ```bash
104
+ python scripts/vendor_repl.py
105
+ ```
106
+
107
+ The `grill` branch is used by default. Use the `branch` argument to specify a different one:
108
+
109
+ ```bash
110
+ python scripts/vendor_repl.py --branch custom/feature-branch
111
+ ```
112
+
113
+ This requires [git](https://git-scm.com/) and [Bun](https://bun.sh/).
@@ -0,0 +1,12 @@
1
+ """Demo module for autodoc doctest REPL integration."""
2
+
3
+ def example_generator(n):
4
+ """Generators yield values useful for iteration.
5
+
6
+ Example:
7
+
8
+ >>> print([i for i in example_generator(4)])
9
+ [0, 1, 2, 3]
10
+
11
+ """
12
+ yield from range(n)
@@ -1,7 +1,11 @@
1
1
  from datetime import date
2
+ import sys
3
+ from pathlib import Path
2
4
 
3
5
  from sphinx_pyrepl_web import __version__
4
6
 
7
+ sys.path.insert(0, str(Path(__file__).parent / "_static"))
8
+
5
9
  project = "sphinx-pyrepl-web"
6
10
  version = __version__
7
11
  author = "Christian López Barrón"
@@ -9,8 +13,11 @@ copyright = f"{date.today().year}, {author}"
9
13
 
10
14
  extensions = [
11
15
  "myst_parser",
16
+ "sphinx.ext.autodoc",
17
+ "sphinx.ext.napoleon",
12
18
  "sphinx_pyrepl_web",
13
19
  ]
20
+ pyrepl_doctest_blocks = "autodoc"
14
21
  exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
15
22
 
16
23
  html_sidebars = {
@@ -31,24 +31,27 @@ Startup script
31
31
  The ``:src:`` option loads a Python script into the REPL namespace. If the script
32
32
  defines a ``setup()`` function, its output is shown when the REPL starts.
33
33
 
34
+ Startup script:
35
+
36
+ .. literalinclude:: _static/setup.py
37
+ :language: python
38
+
39
+ RST content:
40
+
34
41
  .. code-block:: rst
35
42
 
36
43
  .. py-repl::
37
44
  :src: _static/setup.py
38
45
 
46
+ Rendered result:
47
+
39
48
  .. py-repl::
40
49
  :src: _static/setup.py
41
50
 
42
- The startup script:
43
-
44
- .. literalinclude:: _static/setup.py
45
- :language: python
46
-
47
51
  Replay session
48
52
  --------------
49
53
 
50
- Inline directive content is replayed with ``>>>`` prompts, syntax highlighting,
51
- and live output. Doctest-style ``>>>`` prefixes are stripped automatically.
54
+ Inline directive content should follow Doctest-style (``>>>`` / ``...``) and is used as replay prompts.
52
55
 
53
56
  .. code-block:: rst
54
57
 
@@ -59,6 +62,10 @@ and live output. Doctest-style ``>>>`` prefixes are stripped automatically.
59
62
  >>> x = 2 + 2
60
63
  >>> print(f"{x=}")
61
64
  >>> x * 10
65
+ >>> class Foo:
66
+ ... x = 1
67
+ ...
68
+ >>> Foo()
62
69
 
63
70
  .. py-repl::
64
71
  :no-header:
@@ -67,6 +74,10 @@ and live output. Doctest-style ``>>>`` prefixes are stripped automatically.
67
74
  >>> x = 2 + 2
68
75
  >>> print(f"{x=}")
69
76
  >>> x * 10
77
+ >>> class Foo:
78
+ ... x = 1
79
+ ...
80
+ >>> Foo()
70
81
 
71
82
  Combine a silent bootstrap file with a visible replay body:
72
83
 
@@ -84,7 +95,14 @@ Combine a silent bootstrap file with a visible replay body:
84
95
 
85
96
  >>> print(message)
86
97
 
87
- Use ``:replay:`` on ``:src:`` to replay a file with prompts instead of silent load:
98
+ Use ``:replay:`` on ``:src:`` to source a file as replay.
99
+
100
+ Source script:
101
+
102
+ .. literalinclude:: _static/replay_demo.py
103
+ :language: python
104
+
105
+ RST content:
88
106
 
89
107
  .. code-block:: rst
90
108
 
@@ -94,13 +112,32 @@ Use ``:replay:`` on ``:src:`` to replay a file with prompts instead of silent lo
94
112
  :no-header:
95
113
  :no-banner:
96
114
 
115
+ Rendered result:
116
+
97
117
  .. py-repl::
98
118
  :src: _static/replay_demo.py
99
119
  :replay:
100
120
  :no-header:
101
121
  :no-banner:
102
122
 
103
- The replay script:
123
+ Autodoc
124
+ -------
104
125
 
105
- .. literalinclude:: _static/replay_demo.py
126
+ The documented module's source is loaded in advance before replay, so
127
+ module members are available in the REPL namespace. Modules under the Sphinx
128
+ source tree use silent ``:src:``; installed packages use ``packages=``.
129
+
130
+ Source module:
131
+
132
+ .. literalinclude:: _static/autodoc_demo.py
106
133
  :language: python
134
+
135
+ RST content:
136
+
137
+ .. code-block:: rst
138
+
139
+ .. autofunction:: autodoc_demo.example_generator
140
+
141
+ Rendered result:
142
+
143
+ .. autofunction:: autodoc_demo.example_generator
@@ -38,3 +38,8 @@ test = [
38
38
  docs = [
39
39
  "myst-parser",
40
40
  ]
41
+
42
+ [tool.pytest.ini_options]
43
+ filterwarnings = [
44
+ "ignore:The mapping interface for autodoc options objects is deprecated:sphinx.deprecation.RemovedInSphinx11Warning",
45
+ ]
@@ -0,0 +1,316 @@
1
+ """A Sphinx extension for embedding pyrepl-web Python REPLs in documentation."""
2
+
3
+ __version__ = "0.2.0"
4
+
5
+ import importlib
6
+ import inspect
7
+ import json
8
+ from doctest import DocTestParser
9
+ from pathlib import Path
10
+ import sys
11
+
12
+ from docutils import nodes
13
+ from docutils.parsers.rst import directives
14
+ from sphinx import addnodes
15
+ from sphinx.application import Sphinx
16
+ from sphinx.util import logging
17
+ from sphinx.util.docutils import SphinxDirective
18
+ from sphinx.util.fileutil import copy_asset_file
19
+
20
+ PYREPL_DIR = Path(__file__).parent / "pyrepl"
21
+ STARTUP_FILES_KEY = "pyrepl-startup-files"
22
+ REPLAY_FILES_KEY = "pyrepl-replay-files"
23
+ _DOCTEST_PARSER = DocTestParser()
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def setup(app: Sphinx):
28
+ """Setup the extension."""
29
+ app.add_config_value("pyrepl_js", "../pyrepl.js", "env")
30
+ app.add_config_value("pyrepl_doctest_blocks", False, "env")
31
+ app.add_config_value("pyrepl_autodoc_bootstrap", True, "env")
32
+ app.add_directive("py-repl", PyRepl)
33
+ app.connect("doctree-read", doctree_read)
34
+ app.connect("doctree-read", transform_doctest_blocks)
35
+ app.connect("html-page-context", add_html_context)
36
+ app.connect("env-updated", copy_asset_files)
37
+ return {"version": __version__, "parallel_read_safe": True}
38
+
39
+
40
+ def doctest_to_replay_source(text_or_lines: str | list[str]) -> str:
41
+ """Convert doctest-formatted text into executable replay script source."""
42
+ text = (
43
+ "\n".join(text_or_lines)
44
+ if isinstance(text_or_lines, list)
45
+ else text_or_lines
46
+ )
47
+ examples = _DOCTEST_PARSER.get_examples(text)
48
+ if not examples:
49
+ return ""
50
+ parts = [example.source.rstrip("\n") for example in examples]
51
+ return "\n\n".join(parts) + "\n"
52
+
53
+
54
+ def _next_replay_counter(replay_files: dict[str, str]) -> int:
55
+ return len(replay_files) + 1
56
+
57
+
58
+ def register_autodoc_repl(
59
+ env,
60
+ docname: str,
61
+ replay_text: str,
62
+ ) -> str:
63
+ """Record a replay script in env metadata and return its replay-src path."""
64
+ replay_files = json.loads(
65
+ env.metadata[docname].setdefault(REPLAY_FILES_KEY, "{}")
66
+ )
67
+ counter = _next_replay_counter(replay_files)
68
+ replay_name = f"{docname.replace('/', '-')}-{counter}.py"
69
+ replay_files[replay_name] = replay_text
70
+ env.metadata[docname][REPLAY_FILES_KEY] = json.dumps(replay_files)
71
+ return f"_static/pyrepl/{replay_name}"
72
+
73
+
74
+ def register_startup_file(env, docname: str, path: Path) -> str:
75
+ """Track a startup script under srcdir for copying into HTML output."""
76
+ env.note_dependency(path)
77
+ rel_src = path.relative_to(Path(env.srcdir)).as_posix()
78
+ startup_files = json.loads(
79
+ env.metadata[docname].setdefault(STARTUP_FILES_KEY, "[]")
80
+ )
81
+ abs_path = str(path.resolve())
82
+ if abs_path not in startup_files:
83
+ startup_files.append(abs_path)
84
+ env.metadata[docname][STARTUP_FILES_KEY] = json.dumps(startup_files)
85
+ return rel_src
86
+
87
+
88
+ def make_pyrepl_raw(
89
+ replay_src: str,
90
+ src: str | None = None,
91
+ packages: str | None = None,
92
+ ) -> nodes.raw:
93
+ """Build a raw HTML node for an autodoc doctest replay widget."""
94
+ attrs = ["no-header", "no-banner", f'replay-src="{replay_src}"']
95
+ if packages:
96
+ attrs.insert(0, f'packages="{packages}"')
97
+ if src:
98
+ attrs.insert(0, f'src="{src}"')
99
+ attr_str = " ".join(attrs)
100
+ return nodes.raw("", f"<py-repl {attr_str}></py-repl>\n", format="html")
101
+
102
+
103
+ def _find_autodoc_desc(node: nodes.Node) -> addnodes.desc | None:
104
+ """Return the enclosing autodoc desc node, if any."""
105
+ current = node.parent
106
+ while current is not None:
107
+ if isinstance(current, addnodes.desc):
108
+ return current
109
+ current = current.parent
110
+ return None
111
+
112
+
113
+ def _resolve_autodoc_bootstrap(
114
+ app: Sphinx, env, docname: str, desc: addnodes.desc
115
+ ) -> tuple[str | None, str | None]:
116
+ """Return (startup src path, packages) for autodoc REPLs."""
117
+ if not app.config.pyrepl_autodoc_bootstrap:
118
+ return None, None
119
+
120
+ sig = desc.next_node(addnodes.desc_signature)
121
+ if sig is None:
122
+ return None, None
123
+
124
+ module_name = sig.get("module")
125
+ fullname = sig.get("fullname")
126
+ if not module_name:
127
+ return None, None
128
+
129
+ target = f"{module_name}.{fullname}" if fullname else module_name
130
+ try:
131
+ mod = sys.modules.get(module_name)
132
+ if mod is None:
133
+ mod = importlib.import_module(module_name)
134
+ obj = mod
135
+ if fullname:
136
+ for part in fullname.split("."):
137
+ obj = getattr(obj, part)
138
+ mod_obj = inspect.getmodule(obj) or mod
139
+ source_path = Path(inspect.getfile(mod_obj)).resolve()
140
+ srcdir = Path(env.srcdir).resolve()
141
+ try:
142
+ source_path.relative_to(srcdir)
143
+ return register_startup_file(env, docname, source_path), None
144
+ except ValueError:
145
+ return None, module_name.split(".")[0]
146
+ except (AttributeError, ImportError, OSError, TypeError) as exc:
147
+ logger.error(
148
+ "Could not bootstrap autodoc REPL for %s: %s",
149
+ target,
150
+ exc,
151
+ )
152
+ return None, None
153
+
154
+
155
+ def _inside_autodoc_desc(node: nodes.Node) -> bool:
156
+ """Return True if *node* is nested inside an autodoc desc entry."""
157
+ return _find_autodoc_desc(node) is not None
158
+
159
+
160
+ def transform_doctest_blocks(app: Sphinx, doctree: nodes.document):
161
+ """Replace doctest blocks with interactive py-repl widgets."""
162
+ scope = app.config.pyrepl_doctest_blocks
163
+ if not scope:
164
+ return
165
+
166
+ env = app.env
167
+ docname = env.docname
168
+ replaced = False
169
+ for node in doctree.findall(nodes.doctest_block):
170
+ if scope == "autodoc" and not _inside_autodoc_desc(node):
171
+ continue
172
+ source = doctest_to_replay_source(node.astext())
173
+ if not source.strip():
174
+ continue
175
+ bootstrap_src = None
176
+ packages = None
177
+ desc = _find_autodoc_desc(node)
178
+ if desc is not None:
179
+ bootstrap_src, packages = _resolve_autodoc_bootstrap(
180
+ app, env, docname, desc
181
+ )
182
+ replay_src = register_autodoc_repl(env, docname, source)
183
+ node.replace_self(make_pyrepl_raw(replay_src, bootstrap_src, packages))
184
+ replaced = True
185
+
186
+ if replaced:
187
+ env.metadata[docname]["pyrepl"] = True
188
+ doctree["pyrepl"] = True
189
+
190
+
191
+ class PyRepl(SphinxDirective):
192
+ """Embed a pyrepl-web ``<py-repl>`` element."""
193
+
194
+ has_content = True
195
+ option_spec = {
196
+ "theme": directives.unchanged,
197
+ "packages": directives.unchanged,
198
+ "repl-title": directives.unchanged,
199
+ "src": directives.path,
200
+ "no-header": directives.flag,
201
+ "no-buttons": directives.flag,
202
+ "readonly": directives.flag,
203
+ "no-banner": directives.flag,
204
+ "replay": directives.flag,
205
+ "silent": directives.flag,
206
+ }
207
+
208
+ def run(self):
209
+ env = self.env
210
+ attrs: list[str] = []
211
+
212
+ for option, attr in (
213
+ ("theme", "theme"),
214
+ ("packages", "packages"),
215
+ ("repl-title", "repl-title"),
216
+ ):
217
+ if option in self.options:
218
+ value = self.options[option]
219
+ attrs.append(f'{attr}="{value}"')
220
+
221
+ for flag in ("no-header", "no-buttons", "readonly", "no-banner"):
222
+ if flag in self.options:
223
+ attrs.append(flag)
224
+
225
+ has_body = bool(self.content)
226
+ force_replay = "replay" in self.options
227
+ force_silent = "silent" in self.options
228
+
229
+ if "src" in self.options:
230
+ _, abs_path = self.env.relfn2path(self.options["src"])
231
+ path = Path(abs_path)
232
+ try:
233
+ path.read_text(encoding="utf-8")
234
+ except OSError as exc:
235
+ raise self.error(f"Could not read file: {exc}") from exc
236
+ self.env.note_dependency(path)
237
+ rel_src = path.relative_to(Path(self.env.srcdir)).as_posix()
238
+ startup_files = json.loads(
239
+ self.env.metadata[self.env.docname].setdefault(
240
+ STARTUP_FILES_KEY, "[]"
241
+ )
242
+ )
243
+ startup_files.append(str(path))
244
+ self.env.metadata[self.env.docname][STARTUP_FILES_KEY] = json.dumps(
245
+ startup_files
246
+ )
247
+
248
+ if force_replay and not has_body:
249
+ attrs.append(f'src="{rel_src}"')
250
+ attrs.append("replay")
251
+ elif not (force_silent and not has_body):
252
+ attrs.append(f'src="{rel_src}"')
253
+
254
+ if has_body:
255
+ body_text = doctest_to_replay_source(list(self.content))
256
+ replay_src = register_autodoc_repl(env, env.docname, body_text)
257
+ attrs.append(f'replay-src="{replay_src}"')
258
+
259
+ self.env.metadata[self.env.docname]["pyrepl"] = True
260
+ attr_str = (" " + " ".join(attrs)) if attrs else ""
261
+ return [nodes.raw("", f"<py-repl{attr_str}></py-repl>\n", format="html")]
262
+
263
+
264
+ def doctree_read(app: Sphinx, doctree: nodes.document):
265
+ """Mark pages that use the py-repl directive."""
266
+ if app.env.metadata[app.env.docname].get("pyrepl"):
267
+ doctree["pyrepl"] = True
268
+
269
+
270
+ def add_html_context(
271
+ app: Sphinx, pagename: str, templatename: str, context, doctree: nodes.document
272
+ ):
273
+ """Load pyrepl-web JavaScript on pages that contain a REPL."""
274
+ if doctree and "pyrepl" in doctree:
275
+ app.add_js_file(app.config.pyrepl_js)
276
+
277
+
278
+ def copy_asset_files(app, _):
279
+ """Copy vendored pyrepl assets and startup scripts into HTML output."""
280
+ if app.builder.format != "html":
281
+ return
282
+
283
+ outdir = Path(app.builder.outdir)
284
+ if PYREPL_DIR.is_dir():
285
+ for asset in PYREPL_DIR.iterdir():
286
+ if asset.is_file():
287
+ copy_asset_file(str(asset.resolve()), str(outdir.resolve()))
288
+
289
+ replay_dest = outdir / "_static" / "pyrepl"
290
+ for docname, metadata in app.env.metadata.items():
291
+ raw = metadata.get(REPLAY_FILES_KEY)
292
+ if not raw:
293
+ continue
294
+ replay_files = json.loads(raw)
295
+ if not replay_files:
296
+ continue
297
+ replay_dest.mkdir(parents=True, exist_ok=True)
298
+ for name, content in replay_files.items():
299
+ (replay_dest / name).write_text(content, encoding="utf-8")
300
+
301
+ srcdir = Path(app.builder.srcdir)
302
+ copied = set()
303
+ for docname, metadata in app.env.metadata.items():
304
+ raw = metadata.get(STARTUP_FILES_KEY)
305
+ if not raw:
306
+ continue
307
+ for abs_path in json.loads(raw):
308
+ path = Path(abs_path)
309
+ key = str(path.resolve())
310
+ if key in copied:
311
+ continue
312
+ copied.add(key)
313
+ rel = path.relative_to(srcdir)
314
+ dest = outdir / rel
315
+ dest.parent.mkdir(parents=True, exist_ok=True)
316
+ copy_asset_file(str(path.resolve()), str(dest.resolve()))