Skip to content

Commit 2a132cf

Browse files
ENH: scaffold xeus-python kernel for the browser docs
1 parent 346d5a1 commit 2a132cf

4 files changed

Lines changed: 155 additions & 0 deletions

File tree

doc/changes/dev/14093.other.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added a xeus-python kernel environment and setup cell for the JupyterLite documentation, by `Natneal B`_.

doc/jupyterlite_environment.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Environment for the xeus-python JupyterLite kernel used by the browser docs.
2+
#
3+
# Unlike the Pyodide kernel (which installs MNE at runtime via piplite), the
4+
# xeus kernel PRE-INSTALLS everything at build time from these channels:
5+
# - emscripten-forge : WASM-compiled binaries (numpy, scipy, vtk, ...)
6+
# - conda-forge : pure-Python (noarch) packages (nibabel, pooch, mne, ...)
7+
# MNE itself is installed from the local checkout via the pip section (pure
8+
# Python, so no dependency resolution is needed -- its deps are the conda
9+
# packages listed below). Verified to resolve for emscripten-wasm32 2026-07-22.
10+
name: xeus-python
11+
channels:
12+
- https://repo.prefix.dev/emscripten-forge-4x
13+
- https://repo.prefix.dev/conda-forge
14+
dependencies:
15+
- xeus-python
16+
# compiled scientific stack (from emscripten-forge)
17+
- numpy
18+
- scipy
19+
- matplotlib
20+
- pandas
21+
- scikit-learn
22+
- h5py
23+
- statsmodels
24+
# pure-Python deps (from conda-forge)
25+
- nibabel
26+
- pooch
27+
- tqdm
28+
- decorator
29+
- jinja2
30+
- lazy_loader
31+
- packaging
32+
- pymatreader
33+
- h5io
34+
- python-picard
35+
- patsy
36+
- threadpoolctl
37+
- joblib
38+
- pillow
39+
# the development MNE from this checkout (pure Python, installed w/o deps)
40+
- pip:
41+
- ..

doc/jupyterlite_xeus_setup_cell.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"""First-cut JupyterLite setup cell for the xeus-python kernel.
2+
3+
WIP / CI-PENDING -- this is the xeus counterpart of the Pyodide
4+
``first_notebook_cell`` in ``conf.py``. It cannot be fully validated locally
5+
(needs the real ``build_docs`` CI run); the assumptions marked ``CI-VERIFY``
6+
below are the ones to confirm/iterate on first.
7+
8+
Why it is so much smaller than the Pyodide cell
9+
------------------------------------------------
10+
xeus-python is real CPython compiled to WebAssembly (NOT Pyodide), and the
11+
packages are PRE-INSTALLED at build time from ``jupyterlite_environment.yml``.
12+
So compared to the Pyodide setup cell this drops:
13+
14+
* ``piplite.install(...)`` -> MNE is already installed
15+
* the ``lzma`` / ``multiprocessing`` mocks -> real CPython stdlib is present
16+
* the ``pyodide.http`` / ``requests`` patches and the ``js`` XHR fetch shims
17+
-> no Pyodide runtime to patch
18+
19+
What remains is just: point MNE's dataset loaders at the bundled data, block
20+
accidental OSF downloads, and (still to port) the pyvista-js 3D shim.
21+
22+
Data model (CI-VERIFY)
23+
----------------------
24+
With ``XeusAddon.mount_jupyterlite_content=True`` the served JupyterLite content
25+
is mounted into the kernel filesystem at ``/files``. So the curated MNE data
26+
must be made available as JupyterLite content (a conf.py change: add the curated
27+
``mne_data`` tree to ``jupyterlite_contents`` instead of only ``html_extra_path``).
28+
The cell below resolves the data root by probing the likely mount locations so it
29+
is robust to which mechanism ends up being used. Because every bundled file is
30+
then present in the FS, NO runtime fetch is needed -- the loaders just return the
31+
folder (much simpler than the Pyodide lazy-fetch shims).
32+
33+
conf.py wiring this expects (next step, not yet applied so the pyodide build
34+
stays intact on this branch):
35+
36+
from jupyterlite_xeus_setup_cell import XEUS_FIRST_NOTEBOOK_CELL
37+
jupyterlite_build_command_options = {
38+
"XeusAddon.environment_file": "jupyterlite_environment.yml",
39+
"XeusAddon.mount_jupyterlite_content": True,
40+
}
41+
sphinx_gallery_conf["first_notebook_cell"] = XEUS_FIRST_NOTEBOOK_CELL
42+
"""
43+
44+
# Authors: The MNE-Python contributors.
45+
# License: BSD-3-Clause
46+
# Copyright the MNE-Python contributors.
47+
48+
# The setup cell source, as a string (mirrors how conf.py stores the Pyodide one).
49+
XEUS_FIRST_NOTEBOOK_CELL = r"""# 💡 Auto-added setup cell (xeus-python kernel).
50+
# MNE and its dependencies are pre-installed in this kernel; this cell only
51+
# points the dataset loaders at the pre-bundled data.
52+
import os
53+
from pathlib import Path as _Path
54+
import mne
55+
56+
# --- locate the bundled MNE data (mounted from the served jupyterlite content) ---
57+
# CI-VERIFY: confirm the actual mount path (/files with mount_jupyterlite_content).
58+
_candidates = ["/files/mne_data", "/drive/mne_data", os.path.expanduser("~/mne_data")]
59+
mne_data_path = next((p for p in _candidates if os.path.isdir(p)), "/files/mne_data")
60+
os.makedirs(mne_data_path, exist_ok=True)
61+
os.environ["MNE_DATA"] = mne_data_path
62+
63+
# Pre-create a valid empty config so MNE never hits a corrupt read.
64+
_cfg = mne.get_config_path()
65+
os.makedirs(os.path.dirname(_cfg), exist_ok=True)
66+
if not os.path.exists(_cfg):
67+
with open(_cfg, "w") as _f:
68+
_f.write("{}")
69+
mne.set_config("MNE_DATA", mne_data_path)
70+
for _ds in ["SAMPLE", "TESTING", "SSVEP", "EEGBCI", "KILOWORD", "ERP_CORE", "MTRF"]:
71+
mne.set_config(f"MNE_DATASETS_{_ds}_PATH", mne_data_path)
72+
73+
# --- point dataset loaders at the pre-bundled folders (no OSF download) ---
74+
# Everything is already on the filesystem, so these just return the folder --
75+
# no lazy fetching needed (unlike the Pyodide build).
76+
def _lite_folder(_name):
77+
def _data_path(*_a, **_kw):
78+
return _Path(mne_data_path) / _name
79+
return _data_path
80+
81+
mne.datasets.sample.data_path = _lite_folder("MNE-sample-data")
82+
mne.datasets.kiloword.data_path = _lite_folder("MNE-kiloword-data")
83+
mne.datasets.erp_core.data_path = _lite_folder("MNE-ERP-CORE-data")
84+
mne.datasets.mtrf.data_path = _lite_folder("mTRF_1.5")
85+
86+
def _lite_eegbci_load_data(subject, runs, *_a, **_kw):
87+
_runs = [runs] if isinstance(runs, (int, float)) else list(runs)
88+
_subs = list(subject) if isinstance(subject, (list, tuple)) else [subject]
89+
_base = _Path(mne_data_path) / "MNE-eegbci-data" / "files" / "eegmmidb" / "1.0.0"
90+
return [
91+
_base / f"S{int(s):03d}" / f"S{int(s):03d}R{int(r):02d}.edf"
92+
for s in _subs
93+
for r in _runs
94+
]
95+
mne.datasets.eegbci.load_data = _lite_eegbci_load_data
96+
97+
# Block accidental OSF downloads (data is pre-bundled or simply unavailable here).
98+
import pooch
99+
_orig_pooch_fetch = pooch.Pooch.fetch
100+
def _lite_pooch_fetch(self, fname, processor=None, downloader=None):
101+
if "osf.io" in self.get_url(fname):
102+
raise RuntimeError(
103+
f"Cannot download {fname!r} in JupyterLite: open this notebook from "
104+
"mne.tools where the data is pre-bundled, or run it locally."
105+
)
106+
return _orig_pooch_fetch(self, fname, processor=processor, downloader=downloader)
107+
pooch.Pooch.fetch = _lite_pooch_fetch
108+
109+
# TODO(CI-VERIFY): port the pyvista-js SourceEstimate.plot 3D shim from the
110+
# Pyodide conf.py cell -- the approach (JS-native pyvista-js) is kernel-agnostic
111+
# per the plan, only the import/interop path needs confirming under xeus.
112+
"""

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ doc = [
99
"graphviz",
1010
"intersphinx_registry >= 0.2405.27",
1111
"ipython != 8.7.0", # also in "full-no-qt" and "test"
12+
"jupyterlite-xeus",
1213
"memory_profiler >= 0.16",
1314
"mne-bids",
1415
"mne-connectivity",

0 commit comments

Comments
 (0)