|
549 | 549 | with open(pyproject_path, encoding="utf-8") as f: |
550 | 550 | orig_pyproject = f.read() |
551 | 551 |
|
552 | | -# Relax constraints for Pyodide which often lags behind PyPI |
553 | | -patched = re.sub(r'"scipy\s*>=\s*1\.13"', '"scipy >= 1.11"', orig_pyproject) |
554 | | -patched = re.sub(r'"matplotlib\s*>=\s*3\.9"', '"matplotlib >= 3.5"', patched) |
555 | | -patched = re.sub(r'"numpy\s*>=\s*1\.26,\s*<\s*3"', '"numpy >= 1.21, < 3"', patched) |
556 | | -with open(pyproject_path, "w", encoding="utf-8") as f: |
557 | | - f.write(patched) |
558 | | - |
| 552 | +# Relax constraints for Pyodide which often lags behind PyPI. |
| 553 | +# The wheel built here is served to the browser kernel; micropip's |
| 554 | +# keep_going=True means these bounds won't block install, but we also |
| 555 | +# relax them here so the wheel metadata is accurate for inspection. |
| 556 | +patched = re.sub(r'"scipy\s*>=\s*1\.1[0-9]"', '"scipy >= 1.7"', orig_pyproject) |
| 557 | +patched = re.sub(r'"matplotlib\s*>=\s*3\.[5-9]"', '"matplotlib >= 3.5"', patched) |
| 558 | +patched = re.sub(r'"numpy\s*>=\s*1\.\d+,\s*<\s*3"', '"numpy >= 1.20, < 3"', patched) |
559 | 559 | os.environ["SETUPTOOLS_SCM_PRETEND_VERSION"] = "9999.0.1" |
560 | | -subprocess.run( |
561 | | - [sys.executable, "-m", "pip", "wheel", "..", "--no-deps", "-w", dist_lite_dir], |
562 | | - check=True, |
563 | | -) |
564 | | -with open(pyproject_path, "w", encoding="utf-8") as f: |
565 | | - f.write(orig_pyproject) |
| 560 | +try: |
| 561 | + with open(pyproject_path, "w", encoding="utf-8") as f: |
| 562 | + f.write(patched) |
| 563 | + subprocess.run( |
| 564 | + [ |
| 565 | + sys.executable, |
| 566 | + "-m", |
| 567 | + "pip", |
| 568 | + "wheel", |
| 569 | + "..", |
| 570 | + "--no-deps", |
| 571 | + "--no-build-isolation", |
| 572 | + "-w", |
| 573 | + dist_lite_dir, |
| 574 | + ], |
| 575 | + check=True, |
| 576 | + ) |
| 577 | +finally: |
| 578 | + with open(pyproject_path, "w", encoding="utf-8") as f: |
| 579 | + f.write(orig_pyproject) |
566 | 580 |
|
567 | 581 | jupyterlite_build_command_options = {"piplite-wheels": dist_lite_dir} |
568 | 582 |
|
|
573 | 587 | }, |
574 | 588 | "first_notebook_cell": ( |
575 | 589 | "# 💡 This cell is automatically added to the start of each notebook.\n" |
576 | | - "import piplite\n" |
577 | | - "# Install mne without a version pin so piplite resolves the local\n" |
578 | | - "# dev wheel from all.json when available, and falls back cleanly to\n" |
579 | | - "# the latest stable release from PyPI in restricted preview\n" |
580 | | - "# environments (e.g. CircleCI artifact viewer) where the local index\n" |
581 | | - "# is blocked by the host's authentication proxy.\n" |
582 | | - "await piplite.install(['mne', 'scikit-learn', 'joblib'])\n" |
| 590 | + "# It installs MNE and patches the browser environment for Pyodide.\n" |
| 591 | + "import micropip\n" |
| 592 | + "# keep_going=True lets micropip install even if Pyodide's bundled\n" |
| 593 | + "# matplotlib/scipy/numpy are older than MNE's declared minimums.\n" |
| 594 | + "# MNE's runtime code only checks matplotlib >= 3.7/3.8, so 3.8.4 works.\n" |
| 595 | + "await micropip.install(['mne', 'scikit-learn', 'joblib'], keep_going=True)\n" |
583 | 596 | "\n" |
584 | | - "# Mock lzma since it is missing in Pyodide but required by pooch/joblib\n" |
585 | 597 | "import sys\n" |
| 598 | + "import os\n" |
| 599 | + "import io\n" |
| 600 | + "\n" |
| 601 | + "# Mock lzma — missing in Pyodide but imported by pooch/joblib\n" |
586 | 602 | "import types\n" |
587 | 603 | "class MockLZMA:\n" |
588 | 604 | " LZMAError = Exception\n" |
|
594 | 610 | "if 'lzma' not in sys.modules:\n" |
595 | 611 | " sys.modules['lzma'] = MockLZMA()\n" |
596 | 612 | "\n" |
597 | | - "# Mock multiprocessing since it is missing in Pyodide but required by joblib\n" |
| 613 | + "# Mock multiprocessing — missing in Pyodide but imported by joblib\n" |
598 | 614 | "from unittest.mock import MagicMock\n" |
599 | 615 | "if 'multiprocessing' not in sys.modules:\n" |
600 | 616 | " m = MagicMock()\n" |
|
603 | 619 | " sys.modules['multiprocessing.util'] = m.util\n" |
604 | 620 | " sys.modules['multiprocessing.pool'] = m.pool\n" |
605 | 621 | "\n" |
606 | | - "# 1. Patch networking so pooch can download datasets\n" |
607 | | - "import pooch\n" |
| 622 | + "# Patch requests so pooch can fetch files already on /drive/mne_data.\n" |
| 623 | + "# open_url works for both text and binary in Pyodide >= 0.21.\n" |
608 | 624 | "import requests\n" |
609 | | - "import urllib.request\n" |
610 | | - "import urllib.error\n" |
611 | | - "import io\n" |
612 | | - "\n" |
613 | | - "orig_send = requests.Session.send\n" |
614 | 625 | "import pyodide\n" |
| 626 | + "orig_send = requests.Session.send\n" |
615 | 627 | "def pyodide_send(self, request, **kwargs):\n" |
616 | 628 | " try:\n" |
617 | | - " content = pyodide.http.open_url(request.url).read()\n" |
| 629 | + " buf = pyodide.http.open_url(request.url)\n" |
| 630 | + " content = buf.getvalue() if hasattr(buf, 'getvalue') else buf.read()\n" |
618 | 631 | " if isinstance(content, str):\n" |
619 | 632 | " content = content.encode('utf-8')\n" |
620 | | - " status = 200\n" |
621 | 633 | " except Exception as e:\n" |
622 | | - " print(f'pyodide.http.open_url failed for {request.url}: {e}')\n" |
| 634 | + " print(f'open_url failed for {request.url}: {e}')\n" |
623 | 635 | " return orig_send(self, request, **kwargs)\n" |
624 | 636 | " response = requests.Response()\n" |
625 | | - " response.status_code = status\n" |
| 637 | + " response.status_code = 200\n" |
626 | 638 | " response.url = request.url\n" |
627 | 639 | " response.raw = io.BytesIO(content)\n" |
628 | 640 | " return response\n" |
629 | 641 | "requests.Session.send = pyodide_send\n" |
630 | 642 | "\n" |
631 | | - "import os\n" |
| 643 | + "# Set the data directory: /drive/mne_data is pre-populated by the doc\n" |
| 644 | + "# build (conf.py copies the required sample-data subset there).\n" |
| 645 | + "# If absent (e.g. standalone JupyterLite), fall back to /tmp/mne_data\n" |
| 646 | + "# with a clear warning — do NOT attempt to download ~1.5 GB from OSF.\n" |
632 | 647 | "mne_data_path = (\n" |
633 | 648 | " '/drive/mne_data' if os.path.exists('/drive/mne_data')\n" |
634 | 649 | " else '/tmp/mne_data'\n" |
635 | 650 | ")\n" |
636 | 651 | "os.makedirs(mne_data_path, exist_ok=True)\n" |
637 | 652 | "if mne_data_path == '/tmp/mne_data':\n" |
638 | | - " from pyodide.http import pyfetch\n" |
639 | | - " import tarfile\n" |
640 | | - " sample_dir = os.path.join(mne_data_path, 'MNE-sample-data')\n" |
641 | | - " if not os.path.exists(sample_dir):\n" |
642 | | - " print('Downloading MNE-sample-data via pyfetch...')\n" |
643 | | - " try:\n" |
644 | | - " res = await pyfetch(\n" |
645 | | - " 'https://osf.io/86qa2/download?version=5'\n" |
646 | | - " )\n" |
647 | | - " tar_path = os.path.join(mne_data_path, 'sample.tar.gz')\n" |
648 | | - " with open(tar_path, 'wb') as f:\n" |
649 | | - " f.write(await res.bytes())\n" |
650 | | - " with tarfile.open(tar_path, 'r:gz') as tar:\n" |
651 | | - " tar.extractall(mne_data_path)\n" |
652 | | - " os.remove(tar_path)\n" |
653 | | - " print('MNE-sample-data extracted successfully.')\n" |
654 | | - " except Exception as e:\n" |
655 | | - " print(f'Failed to fetch or extract sample data: {e}')\n" |
| 653 | + " print(\n" |
| 654 | + " '⚠️ MNE sample data not found at /drive/mne_data. '\n" |
| 655 | + " 'Cells that load datasets will raise FileNotFoundError. '\n" |
| 656 | + " 'Open this notebook from the live MNE docs (mne.tools) '\n" |
| 657 | + " 'where sample data is pre-bundled.'\n" |
| 658 | + " )\n" |
656 | 659 | "os.environ['MNE_DATA'] = mne_data_path\n" |
657 | 660 | "os.environ['MNE_DATASETS_SAMPLE_PATH'] = mne_data_path\n" |
658 | 661 | "\n" |
659 | | - "# Intercept pooch to provide helpful errors for large OSF datasets\n" |
| 662 | + "# Block pooch from attempting large OSF downloads in the browser.\n" |
| 663 | + "# The required files are either pre-injected or unavailable.\n" |
| 664 | + "import pooch\n" |
660 | 665 | "orig_pooch_fetch = pooch.Pooch.fetch\n" |
661 | 666 | "def pyodide_pooch_fetch(self, fname, processor=None, downloader=None):\n" |
662 | 667 | " url = self.get_url(fname)\n" |
663 | 668 | " if 'osf.io' in url or 'files.osf.io' in url:\n" |
664 | | - " print(f'Bypassing OSF download for" |
665 | | - " {fname}. Using injected local cache.')\n" |
666 | | - " return orig_pooch_fetch(\n" |
667 | | - " self, fname, processor=processor, downloader=downloader\n" |
668 | | - " )\n" |
| 669 | + " raise RuntimeError(\n" |
| 670 | + " f'Cannot download {fname!r} from OSF in JupyterLite: '\n" |
| 671 | + " 'browser CORS policy and memory limits prevent large '\n" |
| 672 | + " 'dataset downloads. Open this notebook from mne.tools '\n" |
| 673 | + " 'where sample data is pre-bundled, or run it locally.'\n" |
| 674 | + " )\n" |
| 675 | + " return orig_pooch_fetch(self, fname, processor=processor, downloader=downloader)\n" |
669 | 676 | "pooch.Pooch.fetch = pyodide_pooch_fetch\n" |
670 | 677 | "\n" |
671 | | - "# 2. Patch MNEBrowseFigure to auto-display in Pyodide's inline backend\n" |
| 678 | + "# Import MNE and finalize setup.\n" |
672 | 679 | "import mne\n" |
673 | | - "import os\n" |
674 | 680 | "try:\n" |
675 | 681 | " mne.get_config()\n" |
676 | 682 | "except Exception:\n" |
|
679 | 685 | " print('Corrupted MNE config deleted automatically.')\n" |
680 | 686 | " except Exception:\n" |
681 | 687 | " pass\n" |
682 | | - "for ds in ['SAMPLE', 'TESTING', 'SSVEP'," |
683 | | - " 'EEGBCI', 'SOMATO', 'AUDIOVISUAL', 'BRAINSTORM']:\n" |
| 688 | + "for ds in ['SAMPLE', 'TESTING', 'SSVEP', 'EEGBCI', 'SOMATO',\n" |
| 689 | + " 'AUDIOVISUAL', 'BRAINSTORM']:\n" |
684 | 690 | " mne.set_config(f'MNE_DATASETS_{ds}_PATH', mne_data_path)\n" |
| 691 | + "\n" |
| 692 | + "# Switch matplotlib to inline so figures render in the notebook.\n" |
685 | 693 | "import IPython\n" |
686 | 694 | "IPython.get_ipython().run_line_magic('matplotlib', 'inline')\n" |
687 | 695 | "import matplotlib.pyplot as plt\n" |
688 | 696 | "import importlib\n" |
689 | 697 | "viz_utils = importlib.import_module('mne.viz.utils')\n" |
690 | 698 | "orig_plt_show = viz_utils.plt_show\n" |
691 | | - "import matplotlib.pyplot as plt\n" |
692 | 699 | "def pyodide_plt_show(*args, **kwargs):\n" |
693 | 700 | " orig_plt_show(*args, **kwargs)\n" |
694 | 701 | " import IPython.display\n" |
|
0 commit comments