diff --git a/Makefile b/Makefile index 3ce4eda956d..12f4f302a8b 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ test-no-testing-data: in $(PYTESTS) mne test-doc: sample_data testing_data - $(PYTESTS) --tb=short --cov=mne --cov-report=xml --cov-branch --doctest-modules --doctest-ignore-import-errors --doctest-glob='*.rst' ./doc/ --ignore=./doc/auto_examples --ignore=./doc/auto_tutorials --ignore=./doc/_build --ignore=./doc/conf.py --ignore=doc/sphinxext --fulltrace + $(PYTESTS) --tb=short --cov=mne --cov-report=xml --cov-branch --doctest-modules --doctest-ignore-import-errors --doctest-glob='*.rst' ./doc/ --ignore=./doc/auto_examples --ignore=./doc/auto_tutorials --ignore=./doc/_build --ignore=./doc/conf.py --ignore=./doc/jupyter_lite_config.py --ignore=doc/sphinxext --fulltrace pre-commit: @pre-commit run -a --show-diff-on-failure diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d85b5999387..02df54ffd81 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -196,6 +196,10 @@ stages: UV_SYSTEM_PYTHON: 'true' # UsePythonVersion gives us a bare interpreter, not a venv UV_NO_PROGRESS: 'true' AZURE_CI_WINDOWS: 'true' + # Mesa's llvmpipe rasterizer threads can deadlock at process exit on + # Windows (pyvista/setup-headless-display-action#55); the action sets + # this itself on GHA, but here we only run its install script + LP_NUM_THREADS: '0' MNE_CI_KIND: $(TEST_MODE) MNE_TEST_ALLOW_SKIP: $(MNE_TEST_ALLOW_SKIP) timeoutInMinutes: 40 diff --git a/doc/changes/dev/14314.other.rst b/doc/changes/dev/14314.other.rst new file mode 100644 index 00000000000..43a577fb668 --- /dev/null +++ b/doc/changes/dev/14314.other.rst @@ -0,0 +1 @@ +Lock the JupyterLite browser kernel's extra packages at build time instead of installing them from the first notebook cell, by `Teon Brooks`_. diff --git a/doc/conf.py b/doc/conf.py index f28d499f539..e64423e5635 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -505,6 +505,8 @@ # sphinx-gallery writes link to auto_*/... at the root jupyterlite_contents = ["jupyterlite_contents/auto_*"] jupyterlite_bind_ipynb_suffix = False + # bakes the browser's extra packages into the Pyodide lock at build time + jupyterlite_config = "jupyter_lite_config.py" # served at the docs root (/mne_data/...) through html_extra_path below stage_lite_data(curpath / "lite_extra" / "mne_data") diff --git a/doc/jupyter_lite_config.py b/doc/jupyter_lite_config.py new file mode 100644 index 00000000000..07dd5b34af3 --- /dev/null +++ b/doc/jupyter_lite_config.py @@ -0,0 +1,30 @@ +"""Build-time config for the JupyterLite site's Pyodide lock. + +Adds MNE and the packages the notebooks import to the lock, so Pyodide loads +them at kernel initialization. Packages Pyodide already curates keep their +curated version. +""" + +# Authors: The MNE-Python contributors. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. + +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).parent / "sphinxext")) + +from jupyterlite_lock_specs import ( # noqa: E402 + jupyterlite_package_names, + jupyterlite_specs_to_lock, +) + +c = get_config() # noqa: F821 + +c.PyodideLockAddon.enabled = True +# the MNE wheel build_lite_wheel.py builds from this checkout (a tagged +# checkout gives the release): the solve takes it over PyPI, which would +# otherwise supply the latest release through mne-connectivity's mne>=1.6 +c.PyodideLockAddon.wheels = ["pypi"] +c.PyodideLockAddon.specs = ["mne", *jupyterlite_specs_to_lock()] +c.PyodideLockAddon.prefetch_extra = ["mne", *jupyterlite_package_names()] diff --git a/doc/sphinxext/jupyterlite_data.py b/doc/sphinxext/jupyterlite_data.py index 829380b4d3d..8fb06084220 100644 --- a/doc/sphinxext/jupyterlite_data.py +++ b/doc/sphinxext/jupyterlite_data.py @@ -175,9 +175,7 @@ def stage_lite_data(dst_base): sphinx_logger.info( f"[JupyterLite] Served data: {n_copied} files copied, {n_missing} missing" ) - # the development wheel, so the browser installs this MNE rather than the - # PyPI release: doc/sphinxext/build_lite_wheel.py puts it in doc/pypi, where - # the piplite addon indexes it; `make html` runs that first, so this is - # only a fallback for a bare sphinx-build + # the MNE wheel jupyter_lite_config.py locks for the browser: `make html` + # builds it first, so this is only a fallback for a bare sphinx-build wheels = find_wheels() or build_wheel() sphinx_logger.info(f"[JupyterLite] MNE wheel for the browser kernel: {wheels}") diff --git a/doc/sphinxext/jupyterlite_lock_specs.py b/doc/sphinxext/jupyterlite_lock_specs.py new file mode 100644 index 00000000000..ab795596d4b --- /dev/null +++ b/doc/sphinxext/jupyterlite_lock_specs.py @@ -0,0 +1,48 @@ +"""Specs to add to Pyodide's lock for JupyterLite, versus what it already curates. + +Naming a package in ``PyodideLockAddon.specs`` drops its existing pin from +the lock (see ``pyodide_lock.uv_pip_compile.constraints_txt``) and resolves +it fresh from PyPI instead, a smaller and differently versioned set than +Pyodide's own curated wheels. :func:`jupyterlite_specs_to_lock` avoids that +by only including specs Pyodide's own lock does not already satisfy. +""" + +# Authors: The MNE-Python contributors. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. + +import json +import tomllib +import urllib.request +from pathlib import Path + +from jupyterlite_pyodide_kernel.constants import PYODIDE_LOCK_DEFAULT_URL +from packaging.requirements import Requirement +from packaging.utils import canonicalize_name + +_PYPROJECT_TOML = Path(__file__).parents[2] / "pyproject.toml" + + +def jupyterlite_specs(): + """Retrieve the "jupyterlite" dependency group from pyproject.toml.""" + with open(_PYPROJECT_TOML, "rb") as fid: + return tomllib.load(fid)["dependency-groups"]["jupyterlite"] + + +def jupyterlite_package_names(): + """Canonical package names in the "jupyterlite" dependency group.""" + return [canonicalize_name(Requirement(spec).name) for spec in jupyterlite_specs()] + + +def jupyterlite_specs_to_lock(): + """Retrieve specs from "jupyterlite" group Pyodide does not already satisfy.""" + with urllib.request.urlopen(PYODIDE_LOCK_DEFAULT_URL) as fid: + curated = json.load(fid)["packages"] + + to_lock = [] + for spec in jupyterlite_specs(): + req = Requirement(spec) + pkg = curated.get(canonicalize_name(req.name)) + if pkg is None or not req.specifier.contains(pkg["version"]): + to_lock.append(spec) + return to_lock diff --git a/doc/sphinxext/jupyterlite_setup_cell.py b/doc/sphinxext/jupyterlite_setup_cell.py index f0bd1b55c26..bfa631523fe 100644 --- a/doc/sphinxext/jupyterlite_setup_cell.py +++ b/doc/sphinxext/jupyterlite_setup_cell.py @@ -1,12 +1,11 @@ """The setup cell prepended to every JupyterLite notebook. -It installs MNE into the browser kernel and hands over to -``mne.viz.backends._jupyterlite.setup_notebook``, which patches what Pyodide -does not provide. piplite (not micropip) prefers the development MNE wheel -bundled with the docs over PyPI, and ``keep_going`` reports a dependency with -no wheel instead of aborting. ``sys.platform`` is ``"emscripten"`` only inside -Pyodide, so the cell is a no-op in a local kernel and a notebook downloaded -from inside JupyterLite runs unchanged there. +It hands over to ``mne.viz.backends._jupyterlite.setup_notebook``, which +patches what Pyodide does not provide; MNE itself and the packages the +notebooks import are in the Pyodide lock (see ``jupyter_lite_config.py``). +``sys.platform`` is ``"emscripten"`` only inside Pyodide, so the cell is a +no-op in a local kernel and a notebook downloaded from inside JupyterLite runs +unchanged there. The docs build prepends it only to the notebooks copied into the JupyterLite contents, not through ``first_notebook_cell``, which would also put it in the @@ -17,24 +16,15 @@ # License: BSD-3-Clause # Copyright the MNE-Python contributors. -# ruff: noqa: E501 # the install list stays on one line so the cell reads short - -import ast - LITE_SETUP_CELL = """\ -# 💡 Added by the docs build: installs MNE into the browser kernel and adapts -# it to Pyodide. Does nothing outside JupyterLite. +# 💡 Added by the docs build: adapts MNE to Pyodide. Does nothing outside +# JupyterLite. import sys if sys.platform == "emscripten": - import piplite - - await piplite.install(["mne", "scikit-learn", "joblib", "pandas", "seaborn", "mne-connectivity", "nibabel", "pyvista-js", "pyxdf", "mffpy", "python-picard"], keep_going=True) # noqa: E501 from mne.viz.backends._jupyterlite import setup_notebook setup_notebook() """ # nothing else runs this before a reader does, so at least make sure it parses -compile( - LITE_SETUP_CELL, "lite_setup_cell", "exec", flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT -) +compile(LITE_SETUP_CELL, "lite_setup_cell", "exec") diff --git a/mne/datasets/_fetch.py b/mne/datasets/_fetch.py index 6ae66814dc4..0151e60dca8 100644 --- a/mne/datasets/_fetch.py +++ b/mne/datasets/_fetch.py @@ -126,7 +126,8 @@ def fetch_dataset( {'dataset_name': 'sample', 'archive_name': 'MNE-sample-data-processed.tar.gz', 'hash': 'md5:e8f30c4516abdc12a0c08e6bae57409c', - 'url': 'https://osf.io/download/86qa2?version=6', + 'url': 'https://github.com/mne-tools/mne-data/releases/download/' + 'sample-6/MNE-sample-data-processed.tar.gz', 'folder_name': 'MNE-sample-data', 'config_key': 'MNE_DATASETS_SAMPLE_PATH'} diff --git a/mne/datasets/_fsaverage/base.py b/mne/datasets/_fsaverage/base.py index cb69b6092b3..1bbecb3851d 100644 --- a/mne/datasets/_fsaverage/base.py +++ b/mne/datasets/_fsaverage/base.py @@ -77,13 +77,13 @@ def fetch_fsaverage(subjects_dir=None, *, verbose=None): _manifest_check_download( manifest_path=FSAVERAGE_MANIFEST_PATH / "root.txt", destination=subjects_dir, - url="https://osf.io/download/3bxqt?version=2", + url="https://github.com/mne-tools/mne-data/releases/download/fsaverage-1.0/fsaverage-root.zip", hash_="5133fe92b7b8f03ae19219d5f46e4177", ) _manifest_check_download( manifest_path=FSAVERAGE_MANIFEST_PATH / "bem.txt", destination=subjects_dir / "fsaverage", - url="https://osf.io/download/7ve8g?version=4", + url="https://github.com/mne-tools/mne-data/releases/download/fsaverage-1.0/fsaverage-bem.zip", hash_="b31509cdcf7908af6a83dc5ee8f49fb1", ) return fs_dir diff --git a/mne/datasets/_phantom/base.py b/mne/datasets/_phantom/base.py index dcafb4edaf0..a9dcaa0667f 100644 --- a/mne/datasets/_phantom/base.py +++ b/mne/datasets/_phantom/base.py @@ -60,7 +60,7 @@ def fetch_phantom(kind="otaniemi", subjects_dir=None, *, verbose=None): """ phantoms = dict( otaniemi=dict( - url="https://osf.io/download/j5czy?version=1", + url="https://github.com/mne-tools/mne-data/releases/download/phantom_otaniemi-1/phantom_otaniemi.zip", hash="42d17db5b1db3e30327ffb4cf2649de8", ), ) diff --git a/mne/datasets/config.py b/mne/datasets/config.py index 0e2c581f450..8b7ade4e9ff 100644 --- a/mne/datasets/config.py +++ b/mne/datasets/config.py @@ -140,7 +140,7 @@ MNE_DATASETS["fnirs_motor"] = dict( archive_name="MNE-fNIRS-motor-data.tgz", hash="md5:c4935d19ddab35422a69f3326a01fef8", - url="https://osf.io/download/dj3eh?version=1", + url="https://github.com/mne-tools/mne-data/releases/download/fnirs_motor-1/MNE-fNIRS-motor-data.tgz", folder_name="MNE-fNIRS-motor-data", config_key="MNE_DATASETS_FNIRS_MOTOR_PATH", ) @@ -148,7 +148,8 @@ MNE_DATASETS["ucl_opm_auditory"] = dict( archive_name="auditory_OPM_stationary.zip", hash="md5:b2d69aa2d656b960bd0c18968dc1a14d", - url="https://osf.io/download/tp324?version=1", # original is mwrt3 + # original is https://osf.io/mwrt3 + url="https://github.com/mne-tools/mne-data/releases/download/ucl_opm_auditory-1/auditory_OPM_stationary.zip", folder_name="auditory_OPM_stationary", config_key="MNE_DATASETS_UCL_OPM_AUDITORY_PATH", ) @@ -156,7 +157,7 @@ MNE_DATASETS["kiloword"] = dict( archive_name="MNE-kiloword-data.tar.gz", hash="md5:3a124170795abbd2e48aae8727e719a8", - url="https://osf.io/download/qkvf9?version=1", + url="https://github.com/mne-tools/mne-data/releases/download/kiloword-1/MNE-kiloword-data.tar.gz", folder_name="MNE-kiloword-data", config_key="MNE_DATASETS_KILOWORD_PATH", ) @@ -172,7 +173,7 @@ MNE_DATASETS["opm"] = dict( archive_name="MNE-OPM-data.tar.gz", hash="md5:370ad1dcfd5c47e029e692c85358a374", - url="https://osf.io/download/p6ae7?version=2", + url="https://github.com/mne-tools/mne-data/releases/download/opm-2/MNE-OPM-data.tar.gz", folder_name="MNE-OPM-data", config_key="MNE_DATASETS_OPM_PATH", ) @@ -180,7 +181,7 @@ MNE_DATASETS["phantom_kit"] = dict( archive_name="MNE-phantom-KIT-data.tar.gz", hash="md5:7bfdf40bbeaf17a66c99c695640e0740", - url="https://osf.io/download/fb6ya?version=1", + url="https://github.com/mne-tools/mne-data/releases/download/phantom_kit-1/MNE-phantom-KIT-data.tar.gz", folder_name="MNE-phantom-KIT-data", config_key="MNE_DATASETS_PHANTOM_KIT_PATH", ) @@ -188,7 +189,7 @@ MNE_DATASETS["phantom_4dbti"] = dict( archive_name="MNE-phantom-4DBTi.zip", hash="md5:938a601440f3ffa780d20a17bae039ff", - url="https://osf.io/download/v2brw?version=2", + url="https://github.com/mne-tools/mne-data/releases/download/phantom_4dbti-2/MNE-phantom-4DBTi.zip", folder_name="MNE-phantom-4DBTi", config_key="MNE_DATASETS_PHANTOM_4DBTI_PATH", ) @@ -196,7 +197,7 @@ MNE_DATASETS["phantom_kernel"] = dict( archive_name="MNE-phantom-kernel.tar.gz", hash="md5:4e2ad987dac1a20f95bae8ffeb2d41d6", - url="https://osf.io/download/dj7wz?version=1", + url="https://github.com/mne-tools/mne-data/releases/download/phantom_kernel-1/MNE-phantom-kernel.tar.gz", folder_name="MNE-phantom-kernel-data", config_key="MNE_DATASETS_PHANTOM_KERNEL_PATH", ) @@ -204,7 +205,7 @@ MNE_DATASETS["sample"] = dict( archive_name="MNE-sample-data-processed.tar.gz", hash="md5:e8f30c4516abdc12a0c08e6bae57409c", - url="https://osf.io/download/86qa2?version=6", + url="https://github.com/mne-tools/mne-data/releases/download/sample-6/MNE-sample-data-processed.tar.gz", folder_name="MNE-sample-data", config_key="MNE_DATASETS_SAMPLE_PATH", ) @@ -221,7 +222,7 @@ MNE_DATASETS["somato"] = dict( archive_name="MNE-somato-data.tar.gz", hash="md5:9a191907b326b9402341ee7a0d1240d8", - url="https://osf.io/download/tp4sg?version=8", + url="https://github.com/mne-tools/mne-data/releases/download/somato-8/MNE-somato-data.tar.gz", folder_name="MNE-somato-data", config_key="MNE_DATASETS_SOMATO_PATH", ) @@ -229,7 +230,7 @@ MNE_DATASETS["spm"] = dict( archive_name="MNE-spm-face.tar.gz", hash="md5:9f43f67150e3b694b523a21eb929ea75", - url="https://osf.io/download/je4s8?version=2", + url="https://github.com/mne-tools/mne-data/releases/download/spm-2/MNE-spm-face.tar.gz", folder_name="MNE-spm-face", config_key="MNE_DATASETS_SPM_FACE_PATH", ) @@ -241,17 +242,32 @@ folder_name="MNE-visual_92_categories-data", config_key="MNE_DATASETS_VISUAL_92_CATEGORIES_PATH", ) +# four parts, each under GitHub's 2 GiB release-asset limit MNE_DATASETS["visual_92_categories_1"] = dict( archive_name="MNE-visual_92_categories-data-part1.tar.gz", - hash="md5:74f50bbeb65740903eadc229c9fa759f", - url="https://osf.io/download/8ejrs?version=1", + hash="md5:b7ee73c69c1c086354ae370c66b41f0b", + url="https://github.com/mne-tools/mne-data/releases/download/visual_92_categories-2/MNE-visual_92_categories-data-part1.tar.gz", folder_name="MNE-visual_92_categories-data", config_key="MNE_DATASETS_VISUAL_92_CATEGORIES_PATH", ) MNE_DATASETS["visual_92_categories_2"] = dict( archive_name="MNE-visual_92_categories-data-part2.tar.gz", - hash="md5:203410a98afc9df9ae8ba9f933370e20", - url="https://osf.io/download/t4yjp?version=1", + hash="md5:f35b89c76a76f2341e68d60b46578621", + url="https://github.com/mne-tools/mne-data/releases/download/visual_92_categories-2/MNE-visual_92_categories-data-part2.tar.gz", + folder_name="MNE-visual_92_categories-data", + config_key="MNE_DATASETS_VISUAL_92_CATEGORIES_PATH", +) +MNE_DATASETS["visual_92_categories_3"] = dict( + archive_name="MNE-visual_92_categories-data-part3.tar.gz", + hash="md5:94b68c9c3ce267cc82106518129c6221", + url="https://github.com/mne-tools/mne-data/releases/download/visual_92_categories-2/MNE-visual_92_categories-data-part3.tar.gz", + folder_name="MNE-visual_92_categories-data", + config_key="MNE_DATASETS_VISUAL_92_CATEGORIES_PATH", +) +MNE_DATASETS["visual_92_categories_4"] = dict( + archive_name="MNE-visual_92_categories-data-part4.tar.gz", + hash="md5:59b0296b2ef1763481e6e5123b0ca207", + url="https://github.com/mne-tools/mne-data/releases/download/visual_92_categories-2/MNE-visual_92_categories-data-part4.tar.gz", folder_name="MNE-visual_92_categories-data", config_key="MNE_DATASETS_VISUAL_92_CATEGORIES_PATH", ) @@ -259,14 +275,14 @@ MNE_DATASETS["mtrf"] = dict( archive_name="mTRF_1.5.zip", hash="md5:273a390ebbc48da2c3184b01a82e4636", - url="https://osf.io/download/h85s2?version=1", + url="https://github.com/mne-tools/mne-data/releases/download/mtrf-1/mTRF_1.5.zip", folder_name="mTRF_1.5", config_key="MNE_DATASETS_MTRF_PATH", ) MNE_DATASETS["refmeg_noise"] = dict( archive_name="sample_reference_MEG_noise-raw.zip", hash="md5:779fecd890d98b73a4832e717d7c7c45", - url="https://osf.io/download/drt6v?version=1", + url="https://github.com/mne-tools/mne-data/releases/download/refmeg_noise-1/sample_reference_MEG_noise-raw.zip", folder_name="MNE-refmeg-noise-data", config_key="MNE_DATASETS_REFMEG_NOISE_PATH", ) @@ -274,7 +290,7 @@ MNE_DATASETS["ssvep"] = dict( archive_name="ssvep_example_data.zip", hash="md5:af866bbc0f921114ac9d683494fe87d6", - url="https://osf.io/download/z8h6k?version=5", + url="https://github.com/mne-tools/mne-data/releases/download/ssvep-5/ssvep_example_data.zip", folder_name="ssvep-example-data", config_key="MNE_DATASETS_SSVEP_PATH", ) @@ -282,7 +298,7 @@ MNE_DATASETS["erp_core"] = dict( archive_name="MNE-ERP-CORE-data.tar.gz", hash="md5:5866c0d6213bd7ac97f254c776f6c4b1", - url="https://osf.io/download/rzgba?version=1", + url="https://github.com/mne-tools/mne-data/releases/download/erp_core-1/MNE-ERP-CORE-data.tar.gz", folder_name="MNE-ERP-CORE-data", config_key="MNE_DATASETS_ERP_CORE_PATH", ) @@ -290,7 +306,7 @@ MNE_DATASETS["epilepsy_ecog"] = dict( archive_name="MNE-epilepsy-ecog-data.tar.gz", hash="md5:ffb139174afa0f71ec98adbbb1729dea", - url="https://osf.io/download/z4epq?version=1", + url="https://github.com/mne-tools/mne-data/releases/download/epilepsy_ecog-1/MNE-epilepsy-ecog-data.tar.gz", folder_name="MNE-epilepsy-ecog-data", config_key="MNE_DATASETS_EPILEPSY_ECOG_PATH", ) @@ -299,7 +315,7 @@ MNE_DATASETS["fieldtrip_cmc"] = dict( archive_name="SubjectCMC.zip", hash="md5:6f9fd6520f9a66e20994423808d2528c", - url="https://osf.io/download/j9b6s?version=1", + url="https://github.com/mne-tools/mne-data/releases/download/fieldtrip_cmc-1/SubjectCMC.zip", folder_name="MNE-fieldtrip_cmc-data", config_key="MNE_DATASETS_FIELDTRIP_CMC_PATH", ) @@ -308,28 +324,28 @@ MNE_DATASETS["bst_auditory"] = dict( archive_name="bst_auditory.tar.gz", hash="md5:fa371a889a5688258896bfa29dd1700b", - url="https://osf.io/download/5t9n8?version=1", + url="https://github.com/mne-tools/mne-data/releases/download/bst_auditory-1/bst_auditory.tar.gz", folder_name="MNE-brainstorm-data", config_key="MNE_DATASETS_BRAINSTORM_PATH", ) MNE_DATASETS["bst_phantom_ctf"] = dict( archive_name="bst_phantom_ctf.tar.gz", hash="md5:80819cb7f5b92d1a5289db3fb6acb33c", - url="https://osf.io/download/sxr8y?version=1", + url="https://github.com/mne-tools/mne-data/releases/download/bst_phantom_ctf-1/bst_phantom_ctf.tar.gz", folder_name="MNE-brainstorm-data", config_key="MNE_DATASETS_BRAINSTORM_PATH", ) MNE_DATASETS["bst_phantom_elekta"] = dict( archive_name="bst_phantom_elekta.tar.gz", hash="md5:1badccbe17998d18cc373526e86a7aaf", - url="https://osf.io/download/dpcku?version=1", + url="https://github.com/mne-tools/mne-data/releases/download/bst_phantom_elekta-1/bst_phantom_elekta.tar.gz", folder_name="MNE-brainstorm-data", config_key="MNE_DATASETS_BRAINSTORM_PATH", ) MNE_DATASETS["bst_raw"] = dict( archive_name="bst_raw.tar.gz", hash="md5:fa2efaaec3f3d462b319bc24898f440c", - url="https://osf.io/download/9675n?version=2", + url="https://github.com/mne-tools/mne-data/releases/download/bst_raw-2/bst_raw.tar.gz", folder_name="MNE-brainstorm-data", config_key="MNE_DATASETS_BRAINSTORM_PATH", ) @@ -355,7 +371,7 @@ # Zenodo can be slow, so we use the OSF mirror # url=('https://zenodo.org/record/3523071/files/' # 'hf_sef_evoked.tar.gz'), - url="https://osf.io/download/25f8d?version=2", + url="https://github.com/mne-tools/mne-data/releases/download/hf_sef_evoked-2/hf_sef_evoked.tar.gz", folder_name="hf_sef", config_key="MNE_DATASETS_HF_SEF_PATH", ) @@ -373,7 +389,9 @@ MNE_DATASETS["eyelink"] = dict( archive_name="MNE-eyelink-data.zip", hash="md5:68a6323ef17d655f1a659c3290ee1c3f", - url=("https://osf.io/download/xsu4g?version=1"), + url=( + "https://github.com/mne-tools/mne-data/releases/download/eyelink-1/MNE-eyelink-data.zip" + ), folder_name="MNE-eyelink-data", config_key="MNE_DATASETS_EYELINK_PATH", ) diff --git a/mne/datasets/utils.py b/mne/datasets/utils.py index c9b910a58e4..26090edee0b 100644 --- a/mne/datasets/utils.py +++ b/mne/datasets/utils.py @@ -255,9 +255,9 @@ def _download_mne_dataset( # handle case of multiple sub-datasets with different urls if name == "visual_92_categories": dataset_params = [] - for name in ["visual_92_categories_1", "visual_92_categories_2"]: - this_dataset = MNE_DATASETS[name] - this_dataset["dataset_name"] = name + for part in sorted(key for key in MNE_DATASETS if key.startswith(f"{name}_")): + this_dataset = MNE_DATASETS[part] + this_dataset["dataset_name"] = part dataset_params.append(this_dataset) return cast( @@ -444,7 +444,10 @@ def fetch_aparc_sub_parcellation(subjects_dir=None, verbose=None): subjects_dir = get_subjects_dir(subjects_dir, raise_error=True) destination = subjects_dir / "fsaverage" / "label" - urls = dict(lh="https://osf.io/download/p92yb", rh="https://osf.io/download/4kxny") + urls = dict( + lh="https://github.com/mne-tools/mne-data/releases/download/hcp_aparc_sub-1/lh.aparc_sub.annot", + rh="https://github.com/mne-tools/mne-data/releases/download/hcp_aparc_sub-1/rh.aparc_sub.annot", + ) hashes = dict( lh="9e4d8d6b90242b7e4b0145353436ef77", rh="dd6464db8e7762d969fc1d8087cd211b" ) diff --git a/mne/viz/backends/_jupyterlite.py b/mne/viz/backends/_jupyterlite.py index 74aba331ff7..39651556525 100644 --- a/mne/viz/backends/_jupyterlite.py +++ b/mne/viz/backends/_jupyterlite.py @@ -40,6 +40,8 @@ def setup_notebook(data_path="/tmp/mne_data"): Where fetched data files are written; ``MNE_DATA`` is set to it. """ global _base, _data_root + if _data_root is not None: # the cell ran again: the wrappers below would + return # otherwise wrap their own wrapped selves and recurse import js # only exists inside Pyodide # The docs serve the data next to the pages (/mne_data/, via diff --git a/mne/viz/backends/tests/test_renderer.py b/mne/viz/backends/tests/test_renderer.py index 75af51ee5e0..61c846e62f9 100644 --- a/mne/viz/backends/tests/test_renderer.py +++ b/mne/viz/backends/tests/test_renderer.py @@ -4,6 +4,7 @@ import os import platform +import subprocess import sys from contextlib import nullcontext @@ -15,7 +16,6 @@ import mne from mne.datasets import testing from mne.transforms import quat_to_rot, rot_to_quat -from mne.utils import run_subprocess from mne.viz import Figure3D, get_3d_backend, set_3d_backend from mne.viz.backends._utils import ALLOWED_QUIVER_MODES from mne.viz.backends.renderer import _get_renderer @@ -299,7 +299,8 @@ def test_renderer(renderer, monkeypatch): f"assert backend != 'jupyterlite_notebook' or 'vtk' not in sys.modules", ] monkeypatch.setenv("MNE_3D_BACKEND", backend) - run_subprocess(cmd) + proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60) + assert proc.returncode == 0, proc.stderr def test_set_3d_backend_bad(monkeypatch, tmp_path): @@ -588,6 +589,10 @@ def send(self): from mne.viz.backends._jupyterlite import setup_notebook setup_notebook(str(root)) +setup_notebook(str(root)) # running the cell twice must not wrap the wrappers +from mne.viz.backends import _jupyterlite + +assert _jupyterlite._orig["read_raw_kit"] is not mne.io.read_raw_kit assert mne.viz.get_3d_backend() == "jupyterlite_notebook" assert mne.datasets.sample.data_path() == root / "MNE-sample-data" data = root / "MNE-testing-data" @@ -622,4 +627,5 @@ def test_lite_setup_notebook(renderer_lite, tmp_path): ) ) args = [sys.executable, str(script), str(tmp_path / "mne_data"), str(_data_path)] - run_subprocess(args, env=env) + proc = subprocess.run(args, capture_output=True, text=True, env=env, timeout=60) + assert proc.returncode == 0, proc.stderr diff --git a/pyproject.toml b/pyproject.toml index 91ca7ed19a3..2fd89b7d710 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,9 @@ doc = [ # the two resolve together. Below either, the browser drops to Pyodide 0.29 # and its matplotlib 3.8.4, a minor under the 3.9 MNE declares. "jupyterlite-pyodide-kernel >= 0.8", + # [lock] brings pyodide-lock and uv for jupyter_lite_config.py; pyodide-lock + # needs 3.12, which every docs build has + "jupyterlite-pyodide-kernel[lock] >= 0.8; python_version >= '3.12'", "jupyterlite-sphinx >= 0.23", "memory_profiler >= 0.16", "mne-bids", @@ -44,6 +47,21 @@ doc = [ "sphinxcontrib-youtube", ] doc-full = ["rpy2", { include-group = "doc" }] +# Packages installed into the browser Pyodide kernel for JupyterLite notebooks; +# unpinned, since Pyodide's curated version wins where it has one and the rest +# resolve to the latest release (see doc/sphinxext/jupyterlite_lock_specs.py) +jupyterlite = [ + "joblib", + "mffpy", + "mne-connectivity", + "nibabel", + "pandas", + "python-picard", + "pyvista-js", + "pyxdf", + "scikit-learn", + "seaborn", +] # Dependencies in a separate group for uv lockfile generation, currently only used for # the 'old' environment CI test run lockfile_extras = [