Skip to content

Commit f81f53b

Browse files
FIX: reach the files and packages the browser notebooks actually need
Most MNE readers validate a path through _check_fname before opening it, so hooking that covers read_info, read_evokeds, read_cov and the rest at once; read_label, read_epochs and read_raw_edf open directly and are wrapped individually. Also serves talairach.xfm, the auditory/visual labels and the EEGBCI runs CI provides, and installs mffpy and python-picard.
1 parent 47ff070 commit f81f53b

1 file changed

Lines changed: 82 additions & 37 deletions

File tree

doc/conf.py

Lines changed: 82 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,9 @@ def _lite_src(folder, rel):
561561
"SSS/ct_sparse_mgh.fif",
562562
"subjects/sample/mri/T1.mgz",
563563
"subjects/sample/mri/aseg.mgz",
564+
# read_talxfm builds this path itself, so nothing in the tutorials
565+
# names it; plot_alignment needs it to estimate MRI fiducials
566+
"subjects/sample/mri/transforms/talairach.xfm",
564567
"subjects/sample/bem/sample-oct-6-src.fif",
565568
# Head and skull surfaces for plot_alignment. outer_skin.surf is what
566569
# MNE picks first, so serving it makes the browser figure match the
@@ -589,6 +592,12 @@ def _lite_src(folder, rel):
589592
"subjects/sample/surf/lh.curv",
590593
"subjects/sample/label/lh.aparc.annot",
591594
"subjects/sample/label/rh.aparc.annot",
595+
# the auditory/visual ROIs; about nine notebooks build these names with
596+
# an f-string, so a scan of the tutorial text never sees them
597+
"MEG/sample/labels/Aud-lh.label",
598+
"MEG/sample/labels/Aud-rh.label",
599+
"MEG/sample/labels/Vis-lh.label",
600+
"MEG/sample/labels/Vis-rh.label",
592601
]
593602
for req in required_files:
594603
s = _lite_src("MNE-sample-data", req)
@@ -677,6 +686,10 @@ def _lite_copy_tree(folder, rel_dir):
677686
for f in sorted(src.rglob("*")):
678687
if not f.is_file():
679688
continue
689+
# zero-byte members (an .mff carries a couple of lock files) do not
690+
# survive the artifact upload, so listing them only yields a 404
691+
if f.stat().st_size == 0:
692+
continue
680693
size_mb = f.stat().st_size / 1e6
681694
if size_mb > LITE_MAX_FILE_MB:
682695
print(f"[JupyterLite] SKIPPED {folder}/{rel_dir} ({size_mb:.1f} MB)")
@@ -753,10 +766,17 @@ def _lite_copy_tree(folder, rel_dir):
753766
("mTRF_1.5", ["speech_data.mat"]),
754767
(
755768
"MNE-eegbci-data",
769+
# exactly the runs tools/circleci_download.sh fetches: subject 1 runs
770+
# 3/6/10/14 and run 3 for subjects 2-4. Notebooks wanting run 1 or 2 are
771+
# excluded instead, since that data never reaches the CI box.
756772
[
773+
"files/eegmmidb/1.0.0/S001/S001R03.edf",
757774
"files/eegmmidb/1.0.0/S001/S001R06.edf",
758775
"files/eegmmidb/1.0.0/S001/S001R10.edf",
759776
"files/eegmmidb/1.0.0/S001/S001R14.edf",
777+
"files/eegmmidb/1.0.0/S002/S002R03.edf",
778+
"files/eegmmidb/1.0.0/S003/S003R03.edf",
779+
"files/eegmmidb/1.0.0/S004/S004R03.edf",
760780
],
761781
),
762782
):
@@ -799,7 +819,8 @@ def _lite_copy_tree(folder, rel_dir):
799819
"# matplotlib/scipy/numpy are older than MNE's declared minimums.\n"
800820
"await piplite.install(\n"
801821
" ['mne', 'scikit-learn', 'joblib', 'pandas', 'seaborn', "
802-
"'mne-connectivity', 'nibabel', 'pyvista-js', 'pyxdf'],\n"
822+
"'mne-connectivity', 'nibabel', 'pyvista-js', 'pyxdf', 'mffpy', "
823+
"'python-picard'],\n"
803824
" keep_going=True,\n"
804825
")\n"
805826
"\n"
@@ -1090,6 +1111,48 @@ def _lite_copy_tree(folder, rel_dir):
10901111
" _lite_fetch_if_under_mne_data(fname), *_a, **_kw\n"
10911112
" )\n"
10921113
"mne.read_source_spaces = _lite_read_source_spaces\n"
1114+
"# Nearly every MNE reader validates its filename through\n"
1115+
"# _check_fname(must_exist=True) before opening it, so hooking that one\n"
1116+
"# function covers read_info, read_evokeds, read_cov, read_label and the\n"
1117+
"# rest without a wrapper each. Failures stay silent here so MNE still\n"
1118+
"# raises its own, clearer error for a file that genuinely is missing.\n"
1119+
"import mne.utils.check as _mne_check\n"
1120+
"_orig_check_fname = _mne_check._check_fname\n"
1121+
"def _lite_check_fname(fname, overwrite=False, must_exist=False,\n"
1122+
" *_a, **_kw):\n"
1123+
" if must_exist:\n"
1124+
" try:\n"
1125+
" _lite_fetch_if_under_mne_data(fname)\n"
1126+
" except Exception:\n"
1127+
" pass\n"
1128+
" return _orig_check_fname(fname, overwrite, must_exist, *_a, **_kw)\n"
1129+
"_mne_check._check_fname = _lite_check_fname\n"
1130+
"# modules that imported it before now hold their own reference; ones\n"
1131+
"# loaded later (mne lazy-loads most of itself) pick up the patch\n"
1132+
"for _m in list(sys.modules.values()):\n"
1133+
" if (getattr(_m, '__name__', '').startswith('mne')\n"
1134+
" and getattr(_m, '_check_fname', None) is _orig_check_fname):\n"
1135+
" _m._check_fname = _lite_check_fname\n"
1136+
"# read_label, read_epochs and read_raw_edf open their file directly\n"
1137+
"# rather than validating it first, so the hook above never sees them\n"
1138+
"_orig_read_label = mne.read_label\n"
1139+
"def _lite_read_label(filename, *_a, **_kw):\n"
1140+
" return _orig_read_label(\n"
1141+
" _lite_fetch_if_under_mne_data(filename), *_a, **_kw\n"
1142+
" )\n"
1143+
"mne.read_label = _lite_read_label\n"
1144+
"_orig_read_epochs = mne.read_epochs\n"
1145+
"def _lite_read_epochs(fname, *_a, **_kw):\n"
1146+
" return _orig_read_epochs(\n"
1147+
" _lite_fetch_if_under_mne_data(fname), *_a, **_kw\n"
1148+
" )\n"
1149+
"mne.read_epochs = _lite_read_epochs\n"
1150+
"_orig_read_raw_edf = mne.io.read_raw_edf\n"
1151+
"def _lite_read_raw_edf(input_fname, *_a, **_kw):\n"
1152+
" return _orig_read_raw_edf(\n"
1153+
" _lite_fetch_if_under_mne_data(input_fname), *_a, **_kw\n"
1154+
" )\n"
1155+
"mne.io.read_raw_edf = _lite_read_raw_edf\n"
10931156
"_orig_read_bem_solution = mne.read_bem_solution\n"
10941157
"def _lite_read_bem_solution(fname, *_a, **_kw):\n"
10951158
" return _orig_read_bem_solution(\n"
@@ -1121,7 +1184,12 @@ def _lite_copy_tree(folder, rel_dir):
11211184
" with open(_manifest) as _fh:\n"
11221185
" _names = [_n.strip() for _n in _fh if _n.strip()]\n"
11231186
" for _name in _names:\n"
1124-
" _lite_fetch_rel(_rel + '/' + _name)\n"
1187+
" # one unreachable member must not abandon the rest of the\n"
1188+
" # recording; the reader complains if it needed that file\n"
1189+
" try:\n"
1190+
" _lite_fetch_rel(_rel + '/' + _name)\n"
1191+
" except Exception as _e:\n"
1192+
" print('[JupyterLite] skipped ' + _name + ': ' + repr(_e))\n"
11251193
" return mne_data_path + '/' + _rel\n"
11261194
"def _lite_dir_reader(_orig):\n"
11271195
" def _read(fname, *_a, **_kw):\n"
@@ -1136,36 +1204,6 @@ def _lite_copy_tree(folder, rel_dir):
11361204
" return _read\n"
11371205
"mne.io.read_raw_nirx = _lite_dir_reader(mne.io.read_raw_nirx)\n"
11381206
"mne.io.read_raw_egi = _lite_dir_reader(mne.io.read_raw_egi)\n"
1139-
"# mne.Report writes an HTML file and opens a browser, neither of which\n"
1140-
"# means anything here. Writes to a temp dir do work in Pyodide's\n"
1141-
"# in-memory filesystem, so save there, read the HTML back and show it\n"
1142-
"# in an isolated iframe -- the report's own CSS/JS then cannot clash\n"
1143-
"# with the notebook page.\n"
1144-
"_orig_report_save = mne.Report.save\n"
1145-
"def _lite_report_save(self, fname=None, *_a, **_kw):\n"
1146-
" # only the HTML form is previewed; an .hdf5 round-trip still has to\n"
1147-
" # write the file the caller asked for\n"
1148-
" if fname is not None and str(fname).lower().endswith(('.h5', '.hdf5')):\n"
1149-
" return _orig_report_save(self, fname, *_a, **_kw)\n"
1150-
" try:\n"
1151-
" import html as _htmllib\n"
1152-
" import tempfile\n"
1153-
" from IPython.display import HTML as _HTML, display as _display\n"
1154-
" _tmp = os.path.join(tempfile.mkdtemp(), 'report.html')\n"
1155-
" _orig_report_save(self, _tmp, open_browser=False, overwrite=True)\n"
1156-
" with open(_tmp, encoding='utf-8') as _fh:\n"
1157-
" _doc = _fh.read()\n"
1158-
" # wrap in a div so the payload does not start with '<iframe',\n"
1159-
" # which trips IPython's 'use IPython.display.IFrame' warning\n"
1160-
" _display(_HTML(\n"
1161-
" '<div><iframe srcdoc=\"' + _htmllib.escape(_doc) + '\" '\n"
1162-
' \'width="100%" height="500" \'\n'
1163-
" 'style=\"border:1px solid #ccc;\"></iframe></div>'\n"
1164-
" ))\n"
1165-
" except Exception as _e:\n"
1166-
" print('(report preview unavailable: ' + repr(_e) + ')')\n"
1167-
" return fname\n"
1168-
"mne.Report.save = _lite_report_save\n"
11691207
"# the logging tutorial reads a KIT file from inside the installed\n"
11701208
"# package; the wheel excludes mne/**/tests, so stage the served copy\n"
11711209
"# into the path the tutorial builds rather than editing the tutorial\n"
@@ -1857,12 +1895,19 @@ def _lite_copy_tree(folder, rel_dir):
18571895
# copy step and the badge would have nothing to load.
18581896
"examples/datasets/kernel_phantom.py",
18591897
"examples/io/elekta_epochs.py",
1860-
# Report renders its forward/inverse/source-estimate sections through the
1861-
# Brain screenshot path, which the browser renderer has no equivalent for,
1862-
# and three sections round-trip the report through HDF5. The parts that do
1863-
# work are not worth a badge that half-renders, so the whole page is hidden
1864-
# (mne.Report.save itself still previews inline -- see the setup cell).
1898+
# These want EEGBCI runs 1 and 2, which tools/circleci_download.sh never
1899+
# fetches (it takes subject 1 runs 3/6/10/14 and run 3 for subjects 2-4),
1900+
# so the data is not on the machine that builds the docs. eeg_bridging
1901+
# alone would need run 1 for ten subjects.
1902+
"examples/visualization/onionskin.py",
1903+
"examples/preprocessing/muscle_ica.py",
1904+
"examples/preprocessing/eeg_bridging.py",
1905+
# Both Report tutorials build their figures by screenshotting a 3D scene
1906+
# (Report._itv calls backend._take_3d_screenshot), and vtk.js cannot hand a
1907+
# framebuffer back to Python, so those sections would embed blank images.
1908+
# 70_report additionally round-trips a report through HDF5.
18651909
"tutorials/intro/70_report.py",
1910+
"tutorials/preprocessing/14_quality_control_report.py",
18661911
)
18671912

18681913
import sphinx_gallery.gen_rst as _sg_gen_rst # noqa: E402

0 commit comments

Comments
 (0)