Skip to content

Commit 019aa82

Browse files
committed
feat: add support for extracting and loading Pyodide packages in interactive examples
1 parent a035759 commit 019aa82

3 files changed

Lines changed: 44 additions & 5 deletions

File tree

Examples/Interactive/plot_interactive_fitting.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
and the sum curve will jump to the optimal fit.
2020
Click a component line again to hide its widgets.
2121
"""
22+
# Packages required when running interactively in Pyodide (docs live mode).
23+
_PYODIDE_PACKAGES = ["scipy"]
24+
2225
import numpy as np
2326
from scipy.optimize import curve_fit
2427
import anyplotlib as apl

anyplotlib/sphinx_anywidget/_scraper.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@
5151
# Sentinel that marks a code block as interactive.
5252
_INTERACTIVE_RE = re.compile(r"#\s*interactive\s*$", re.IGNORECASE | re.MULTILINE)
5353

54+
# Pattern that extracts _PYODIDE_PACKAGES = [...] declarations from source.
55+
_PYODIDE_PACKAGES_RE = re.compile(
56+
r"^_PYODIDE_PACKAGES\s*=\s*(\[[^\]]*\])", re.MULTILINE
57+
)
58+
5459

5560
# ---------------------------------------------------------------------------
5661
# Helpers
@@ -293,11 +298,28 @@ def __call__(self, block, block_vars, gallery_conf):
293298

294299
if python_src:
295300
data_src = _html_escape(_json.dumps(python_src), quote=True)
301+
302+
# Detect _PYODIDE_PACKAGES = [...] in the source.
303+
_pkg_attr = ""
304+
m = _PYODIDE_PACKAGES_RE.search(python_src)
305+
if m:
306+
try:
307+
import ast as _ast
308+
pkgs = _ast.literal_eval(m.group(1))
309+
if pkgs:
310+
_pkg_attr = (
311+
f' data-pyodide-packages='
312+
f'"{_html_escape(_json.dumps(pkgs), quote=True)}"'
313+
)
314+
except Exception:
315+
pass
316+
296317
python_block = (
297318
f'<script type="text/x-python"'
298319
f' data-fig-id="{fig_id}"'
299320
f' data-fig-index="{fig_index}"'
300321
f' data-src-file="{Path(src_file).stem}"'
322+
f'{_pkg_attr}'
301323
f' data-src="{data_src}"></script>'
302324
)
303325
rst += "\n\n.. raw:: html\n\n " + python_block + "\n\n"

anyplotlib/sphinx_anywidget/static/anywidget_bridge.js

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ print('[sphinx_anywidget] anywidget monkey-patch installed')
237237

238238
// 8. Collect text/x-python script blocks, group by src-file so each
239239
// example source runs exactly once even with multiple figures.
240-
const srcGroups = new Map(); // srcFile → { src, pairs: [{figId, figIndex}] }
240+
const srcGroups = new Map(); // srcFile → { src, pairs: [{figId, figIndex}], packages: [] }
241241

242242
for (const script of document.querySelectorAll(
243243
'script[type="text/x-python"][data-fig-id]')) {
@@ -246,9 +246,14 @@ print('[sphinx_anywidget] anywidget monkey-patch installed')
246246
const figIndex = parseInt(script.dataset.figIndex || '0', 10);
247247
let src = '';
248248
try { src = JSON.parse(script.dataset.src || 'null') || ''; } catch (_) {}
249-
250-
if (!srcGroups.has(srcFile)) srcGroups.set(srcFile, { src, pairs: [] });
251-
srcGroups.get(srcFile).pairs.push({ figId, figIndex });
249+
let packages = [];
250+
try { packages = JSON.parse(script.dataset.pyodidePackages || 'null') || []; } catch (_) {}
251+
252+
if (!srcGroups.has(srcFile)) srcGroups.set(srcFile, { src, pairs: [], packages });
253+
const grp = srcGroups.get(srcFile);
254+
grp.pairs.push({ figId, figIndex });
255+
// Merge any packages declared by any script tag for this source file.
256+
for (const p of packages) if (!grp.packages.includes(p)) grp.packages.push(p);
252257
}
253258

254259
for (const g of srcGroups.values())
@@ -257,10 +262,19 @@ print('[sphinx_anywidget] anywidget monkey-patch installed')
257262
// 9. Run each example source once, assign _anywidget_fig_id in creation
258263
// order, then push current state into the matching iframes.
259264
const _execErrors = [];
260-
for (const [srcFile, { src, pairs }] of srcGroups) {
265+
for (const [srcFile, { src, pairs, packages }] of srcGroups) {
261266
const figIdList = JSON.stringify(pairs.map(p => p.figId));
262267
console.info(`[sphinx_anywidget] running: ${srcFile} (${pairs.length} figure(s))`);
263268
const _srcFileRepr = JSON.stringify(srcFile);
269+
270+
// Load any extra packages declared by this example (e.g. scipy).
271+
if (packages.length > 0) {
272+
console.info(`[sphinx_anywidget] loading extra packages for ${srcFile}:`, packages);
273+
await _step(`load packages for ${srcFile}`,
274+
pyodide.loadPackage(packages));
275+
console.info(`[sphinx_anywidget] extra packages loaded for ${srcFile}`);
276+
}
277+
264278
try {
265279
await pyodide.runPythonAsync(`
266280
import anywidget as _aw

0 commit comments

Comments
 (0)