Skip to content

Commit e37acb3

Browse files
FIX: Patch MNE internals for Pyodide/emscripten compatibility
- mne/parallel.py: return False early in _running_in_joblib_context() on emscripten; joblib parallel backends are unavailable in the browser - mne/utils/config.py: catch Exception (not just ValueError) when loading the MNE config JSON; Pyodide's json parser raises SyntaxError on a corrupt or absent config file
1 parent 641d268 commit e37acb3

2 files changed

Lines changed: 73 additions & 40 deletions

File tree

mne/parallel.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ def parallel_progress(op_iter):
156156

157157
def _running_in_joblib_context():
158158
"""Check if we are running in a joblib.parallel_config context manager."""
159+
import sys
160+
161+
if sys.platform == "emscripten":
162+
return False
159163
try:
160164
from joblib.parallel import get_active_backend
161165
except ImportError:

mne/utils/config.py

Lines changed: 69 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,19 @@
66

77
import atexit
88
import contextlib
9+
import importlib.metadata
10+
import importlib.util
911
import json
1012
import multiprocessing
1113
import os
1214
import os.path as op
1315
import platform
1416
import shutil
17+
import site
1518
import subprocess
1619
import sys
1720
import tempfile
1821
from functools import lru_cache, partial
19-
from importlib import import_module
2022
from pathlib import Path
2123
from urllib.error import URLError
2224
from urllib.request import urlopen
@@ -272,8 +274,8 @@ def _load_config(config_path, raise_error=False):
272274
with _open_lock(config_path, "r+") as fid:
273275
try:
274276
config = json.load(fid)
275-
except ValueError:
276-
# No JSON object could be decoded --> corrupt file?
277+
except Exception:
278+
# Catch ANY exception (including SyntaxError from Pyodide json parser)
277279
msg = (
278280
f"The MNE-Python config file ({config_path}) is not a valid JSON "
279281
"file and might be corrupted"
@@ -757,14 +759,19 @@ def sys_info(
757759
758760
.. versionadded:: 1.6
759761
"""
762+
import matplotlib
763+
760764
_validate_type(dependencies, str)
761765
_check_option("dependencies", dependencies, ("user", "developer"))
762766
_validate_type(check_version, (bool, "numeric"), "check_version")
763767
_validate_type(unicode, (bool, str), "unicode")
764768
_check_option("unicode", unicode, ("auto", True, False))
765769
if unicode == "auto":
766770
if platform.system() in ("Darwin", "Linux"):
767-
unicode = True
771+
try:
772+
unicode = sys.stdout.encoding.lower().startswith("utf")
773+
except Exception: # in case someone overrides sys.stdout in an unsafe way
774+
unicode = False
768775
else: # Windows
769776
unicode = False
770777
ljust = 24 if dependencies == "developer" else 21
@@ -788,6 +795,13 @@ def sys_info(
788795
else:
789796
total_memory = f"{total_memory / 1024**3:.1f}" # convert to GiB
790797
out(f"{total_memory} GiB\n")
798+
site_packages_path = (site.getsitepackages() or [None])[0]
799+
if show_paths and site_packages_path is not None:
800+
out("Site-packages".ljust(ljust) + f"{site_packages_path}\n")
801+
site_packages_path = Path(site_packages_path)
802+
out("".ljust(ljust))
803+
out("└►" if unicode else "^-")
804+
out(" Any paths not listed below are in site-packages")
791805
out("\n")
792806
ljust -= 3 # account for +/- symbols
793807
libs = _get_numpy_libs()
@@ -800,12 +814,13 @@ def sys_info(
800814
"matplotlib",
801815
"",
802816
"# Numerical (optional)",
803-
"sklearn",
817+
"scikit-learn",
804818
"numba",
805819
"nibabel",
806820
"nilearn",
807821
"dipy",
808822
"openmeeg",
823+
"python-picard",
809824
"cupy",
810825
"pandas",
811826
"h5io",
@@ -820,7 +835,7 @@ def sys_info(
820835
"pyqtgraph",
821836
"mne-qt-browser",
822837
"ipywidgets",
823-
# "trame", # no version, see https://github.com/Kitware/trame/issues/183
838+
"trame",
824839
"trame_client",
825840
"trame_server",
826841
"trame_pyvista",
@@ -834,6 +849,7 @@ def sys_info(
834849
"mne-connectivity",
835850
"mne-icalabel",
836851
"mne-bids-pipeline",
852+
"autoreject",
837853
"neo",
838854
"eeglabio",
839855
"edfio",
@@ -849,6 +865,18 @@ def sys_info(
849865
use_mod_names += (
850866
"# Testing",
851867
"pytest",
868+
"pytest-cov",
869+
"pytest-qt",
870+
"pytest-rerunfailures",
871+
"pytest-timeout",
872+
"codespell",
873+
"ipython",
874+
"mypy",
875+
"pillow",
876+
"pre-commit",
877+
"ruff",
878+
"vulture",
879+
"",
852880
"hedtools",
853881
"statsmodels",
854882
"numpydoc",
@@ -859,6 +887,7 @@ def sys_info(
859887
"imageio",
860888
"imageio-ffmpeg",
861889
"snirf",
890+
"twine",
862891
"",
863892
"# Documentation",
864893
"sphinx",
@@ -874,12 +903,24 @@ def sys_info(
874903
"tqdm",
875904
"",
876905
)
877-
try:
878-
unicode = unicode and (sys.stdout.encoding.lower().startswith("utf"))
879-
except Exception: # in case someone overrides sys.stdout in an unsafe way
880-
unicode = False
881-
mne_version_good = True
882-
import_names = dict(hedtools="hed")
906+
if check_version:
907+
timeout = 2.0 if check_version is True else float(check_version)
908+
mne_version_good, mne_extra = _check_mne_version(timeout)
909+
if mne_version_good is None:
910+
mne_version_good = True
911+
del timeout
912+
else:
913+
mne_version_good = True
914+
mne_extra = ""
915+
del check_version
916+
import_names = {
917+
"codespell": "codespell_lib",
918+
"hedtools": "hed",
919+
"ipython": "IPython",
920+
"pillow": "PIL",
921+
"pytest-qt": "pytestqt",
922+
"scikit-learn": "sklearn",
923+
}
883924
for mi, mod_name in enumerate(use_mod_names):
884925
# upcoming break
885926
if mod_name == "": # break
@@ -897,62 +938,50 @@ def sys_info(
897938
continue
898939
pre = "├"
899940
last = use_mod_names[mi + 1] == "" and not unavailable
941+
import_name = import_names.get(mod_name, mod_name).replace("-", "_")
900942
if last:
901943
pre = "└"
902944
try:
903-
import_name = import_names.get(mod_name, mod_name.replace("-", "_"))
904-
mod = import_module(import_name)
945+
ver = importlib.metadata.version(mod_name)
946+
mod_loc = Path(importlib.util.find_spec(import_name).origin)
905947
except Exception:
906948
unavailable.append(mod_name)
907949
else:
950+
if mod_loc.stem == "__init__":
951+
mod_loc = mod_loc.parent
952+
if site_packages_path and mod_loc.is_relative_to(site_packages_path):
953+
mod_loc = None
908954
mark = "☑" if unicode else "+"
909-
mne_extra = ""
910-
if mod_name == "mne" and check_version:
911-
timeout = 2.0 if check_version is True else float(check_version)
912-
mne_version_good, mne_extra = _check_mne_version(timeout)
913-
if mne_version_good is None:
914-
mne_version_good = True
915-
elif not mne_version_good:
916-
mark = "☒" if unicode else "X"
955+
if mod_name == "mne" and not mne_version_good:
956+
mark = "☒" if unicode else "X"
917957
out(f"{pre}{mark} " if unicode else f" {mark} ")
918958
out(f"{mod_name}".ljust(ljust))
919-
if mod_name == "vtk":
920-
vtk_version = mod.vtkVersion()
921-
# 9.0 dev has VersionFull but 9.0 doesn't
922-
for attr in ("GetVTKVersionFull", "GetVTKVersion"):
923-
if hasattr(vtk_version, attr):
924-
version = getattr(vtk_version, attr)()
925-
if version != "":
926-
out(version)
927-
break
928-
else:
929-
out("unknown")
930-
else:
931-
out(mod.__version__.lstrip("v"))
959+
out(ver)
932960
if mod_name == "numpy":
933961
out(f" ({libs})")
934962
elif mod_name == "qtpy":
935963
version, api = _check_qt_version(return_api=True)
936964
out(f" ({api}={version})")
937965
elif mod_name == "matplotlib":
938-
out(f" (backend={mod.get_backend()})")
966+
out(f" (backend={matplotlib.get_backend()})")
939967
elif mod_name == "pyvista":
940968
version, renderer = _get_gpu_info()
941969
if version is None:
942970
out(" (OpenGL unavailable)")
943971
else:
944972
out(f" (OpenGL {version} via {renderer})")
945973
elif mod_name == "mne":
946-
out(f" ({mne_extra})")
974+
if mne_extra:
975+
out(f" ({mne_extra})")
947976
# Now comes stuff after the version
948-
if show_paths:
977+
if show_paths and mod_loc is not None:
949978
if last:
950979
pre = " "
951980
elif unicode:
952981
pre = "│ "
953982
else:
954983
pre = " | "
955-
out(f"\n{pre}{' ' * ljust}{op.dirname(mod.__file__)}")
984+
out(f"\n{pre}{' ' * ljust}{mod_loc}")
956985
out("\n")
957986

958987
if not mne_version_good:
@@ -986,7 +1015,7 @@ def _check_mne_version(timeout):
9861015
if not rel_ver[0].isnumeric():
9871016
return None, (f"unable to check for latest version on GitHub, {rel_ver}")
9881017
rel_ver = parse(rel_ver)
989-
this_ver = parse(import_module("mne").__version__)
1018+
this_ver = parse(importlib.metadata.version("mne"))
9901019
if this_ver > rel_ver:
9911020
return True, f"development, latest release is {rel_ver}"
9921021
if this_ver == rel_ver:

0 commit comments

Comments
 (0)