diff --git a/.github/workflows/jupyterlite.yml b/.github/workflows/jupyterlite.yml new file mode 100644 index 00000000000..c06af5601ae --- /dev/null +++ b/.github/workflows/jupyterlite.yml @@ -0,0 +1,46 @@ +name: Build JupyterLite + +on: # yamllint disable-line rule:truthy + push: + branches: + - jupyterlite-gh-actions + +permissions: + contents: read + +jobs: + build: + name: Build JupyterLite Site + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install JupyterLite & Build Tools + run: | + python -m pip install --upgrade pip + pip install jupyterlite-core jupyterlite-pyodide-kernel build jupyter-server + + - name: Build MNE-Python Wheel + run: | + python -m build --wheel + mkdir -p lite-wheels + cp dist/*.whl lite-wheels/ + + - name: Build JupyterLite Site + # We pass the local wheel to JupyterLite so the browser environment uses the exact code from this branch! + run: | + jupyter lite build --contents examples/ --output-dir dist_lite/ --piplite-wheel lite-wheels/*.whl + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: jupyterlite-build + path: dist_lite/ diff --git a/doc/changes/dev/13925.newfeature.rst b/doc/changes/dev/13925.newfeature.rst new file mode 100644 index 00000000000..bcd3109e8d1 --- /dev/null +++ b/doc/changes/dev/13925.newfeature.rst @@ -0,0 +1 @@ +Added a JupyterLite GitHub Actions workflow to automatically build a Wasm-compatible interactive documentation site by Natneal Belete. diff --git a/doc/conf.py b/doc/conf.py index f14026d21c6..b2d974cdad6 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -113,10 +113,11 @@ # contrib "matplotlib.sphinxext.plot_directive", "numpydoc", + "sphinxcontrib.bibtex", + "sphinx_gallery.gen_gallery", + "jupyterlite_sphinx", "sphinx_copybutton", "sphinx_design", - "sphinx_gallery.gen_gallery", - "sphinxcontrib.bibtex", "sphinxcontrib.youtube", "sphinxcontrib.towncrier.ext", # homegrown @@ -472,7 +473,90 @@ compress_images = () sphinx_gallery_parallel = int(os.getenv("MNE_DOC_BUILD_N_JOBS", "1")) +jupyterlite_contents = ["jupyterlite_contents"] +jupyterlite_bind_ipynb_suffix = False + +# Build the local MNE wheel so JupyterLite can use the current development version +dist_lite_dir = os.path.join( + os.path.abspath(os.path.dirname(__file__)), "_build", "dist_lite" +) +os.makedirs(dist_lite_dir, exist_ok=True) +subprocess.run( + [sys.executable, "-m", "pip", "wheel", "..", "--no-deps", "-w", dist_lite_dir], + check=True, +) + +jupyterlite_build_command_options = {"piplite-wheels": dist_lite_dir} + sphinx_gallery_conf = { + "jupyterlite": { + "use_jupyter_lab": True, + "jupyterlite_contents": "jupyterlite_contents", + }, + "first_notebook_cell": ( + "# 💡 This cell is automatically added to the start of each notebook.\n" + "import piplite\n" + "await piplite.install(['mne'])\n" + "\n" + "# Mock lzma since it is missing in Pyodide but required by pooch\n" + "import sys\n" + "import types\n" + "if 'lzma' not in sys.modules:\n" + " sys.modules['lzma'] = types.ModuleType('lzma')\n" + "\n" + "# 1. Patch networking so pooch can download datasets\n" + "import pooch\n" + "import requests\n" + "import urllib.request\n" + "import urllib.error\n" + "import io\n" + "\n" + "orig_send = requests.Session.send\n" + "def pyodide_send(self, request, **kwargs):\n" + " try:\n" + " resp = urllib.request.urlopen(request.url)\n" + " content = resp.read()\n" + " status = resp.status\n" + " except urllib.error.HTTPError as e:\n" + " content = e.read()\n" + " status = e.code\n" + " except Exception:\n" + " return orig_send(self, request, **kwargs)\n" + " response = requests.Response()\n" + " response.status_code = status\n" + " response.url = request.url\n" + " response.raw = io.BytesIO(content)\n" + " return response\n" + "requests.Session.send = pyodide_send\n" + "\n" + "# Intercept pooch to provide helpful errors for large OSF datasets\n" + "orig_pooch_fetch = pooch.Pooch.fetch\n" + "def pyodide_pooch_fetch(self, fname, processor=None, downloader=None):\n" + " url = self.get_url(fname)\n" + " if 'osf.io' in url or 'files.osf.io' in url:\n" + " raise RuntimeError(\n" + " f'Cannot download {fname} natively in JupyterLite '\n" + " 'due to browser CORS restrictions on OSF and '\n" + " 'memory limits. Please download the dataset locally '\n" + " 'and upload it into the JupyterLite file browser.'\n" + " )\n" + " return orig_pooch_fetch(\n" + " self, fname, processor=processor, downloader=downloader\n" + " )\n" + "pooch.Pooch.fetch = pyodide_pooch_fetch\n" + "\n" + "# 2. Patch MNEBrowseFigure to auto-display in Pyodide's inline backend\n" + "import mne\n" + "import matplotlib.pyplot as plt\n" + "import importlib\n" + "viz_utils = importlib.import_module('mne.viz.utils')\n" + "orig_plt_show = viz_utils.plt_show\n" + "def pyodide_plt_show(fig=None, **kwargs):\n" + " orig_plt_show(fig, **kwargs)\n" + " import IPython.display\n" + " IPython.display.display(plt.gcf())\n" + "viz_utils.plt_show = pyodide_plt_show\n" + ), "doc_module": ("mne",), "reference_url": dict(mne=None), "examples_dirs": examples_dirs, diff --git a/doc/jupyterlite_contents/.gitkeep b/doc/jupyterlite_contents/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/mne/_fiff/meas_info.py b/mne/_fiff/meas_info.py index 32ac823205e..c76e757fff7 100644 --- a/mne/_fiff/meas_info.py +++ b/mne/_fiff/meas_info.py @@ -1298,22 +1298,6 @@ class Info(ValidatedDict, SetChannelsMixin, MontageMixin, ContainsMixin): modified by various MNE-Python functions or methods (which have safeguards to ensure all fields remain in sync). - Some common methods that safely modify the ``info`` object include: - - * :meth:`mne.io.Raw.add_proj`, :meth:`mne.Epochs.add_proj`, - :meth:`mne.Evoked.add_proj` - * :meth:`mne.io.Raw.del_proj`, :meth:`mne.Epochs.del_proj`, - :meth:`mne.Evoked.del_proj` - * :meth:`mne.io.Raw.rename_channels`, - :meth:`mne.Epochs.rename_channels`, - :meth:`mne.Evoked.rename_channels` - * :meth:`mne.io.Raw.set_channel_types`, - :meth:`mne.Epochs.set_channel_types`, - :meth:`mne.Evoked.set_channel_types` - * :meth:`mne.io.Raw.set_meas_date`, - :meth:`mne.Epochs.set_meas_date`, - :meth:`mne.Evoked.set_meas_date` - Parameters ---------- *args : list diff --git a/pyproject.toml b/pyproject.toml index f0e05dfe953..ca5bf63dd39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,8 @@ doc = [ "graphviz", "intersphinx_registry >= 0.2405.27", "ipython != 8.7.0", # also in "full-no-qt" and "test" + "jupyterlite-pyodide-kernel", + "jupyterlite-sphinx", "memory_profiler >= 0.16", "mne-bids", "mne-connectivity",