diff --git a/.circleci/config.yml b/.circleci/config.yml
index a8943cc0094..b705bbc79a7 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -137,8 +137,8 @@ jobs:
- run:
name: Check Qt
command: |
- set -x
- ./tools/check_qt_import.sh ${MNE_QT_BACKEND}
+ curl https://raw.githubusercontent.com/mne-tools/mne-tools/main/tools/check_qt_import.sh -o check_qt_import.sh
+ bash check_qt_import.sh $MNE_QT_BACKEND
# Load tiny cache so that ~/.mne does not need to be created below
- restore_cache:
keys:
diff --git a/.extended_metadata.yaml b/.extended_metadata.yaml
new file mode 100644
index 00000000000..4ca55a54eca
--- /dev/null
+++ b/.extended_metadata.yaml
@@ -0,0 +1,55 @@
+# This file contains additional metadata about MNE-Python, beyond what is contained in `pyproject.toml`.
+# This information is used by the actions in MNE-Tools to generate `CITATION.cff` and `codemeta.json`.
+package_name: MNE-Python
+date_created: '2010-12-26'
+date_published: '2014-08-04'
+application_category: Neuroscience
+code_doi: 10.5281/zenodo.592483
+compound_surnames:
+ - García Alanis
+ - van Vliet
+ - De Santis
+ - Dupré la Tour
+ - de la Torre
+ - de Jong
+ - de Montalivet
+ - van den Bosch
+ - Van den Bossche
+ - Van Der Donckt
+ - van der Meer
+ - van Harmelen
+ - Visconti di Oleggio Castello
+ - van Es
+preferred_citation:
+ title: MEG and EEG Data Analysis with MNE-Python
+ journal: Frontiers in Neuroscience
+ type: article
+ year: 2013
+ volume: 7
+ issue: 267
+ start: 1
+ end: 13
+ doi: 10.3389/fnins.2013.00267
+ authors:
+ - family-names: Gramfort
+ given-names: Alexandre
+ - family-names: Luessi
+ given-names: Martin
+ - family-names: Larson
+ given-names: Eric
+ - family-names: Engemann
+ given-names: Denis A.
+ - family-names: Strohmeier
+ given-names: Daniel
+ - family-names: Brodbeck
+ given-names: Christian
+ - family-names: Goj
+ given-names: Roman
+ - family-names: Jas
+ given-names: Mainak
+ - family-names: Brooks
+ given-names: Teon
+ - family-names: Parkkonen
+ given-names: Lauri
+ - family-names: Hämäläinen
+ given-names: Matti S.
diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml
index 79daf7e789c..dad0968b5ca 100644
--- a/.github/workflows/autofix.yml
+++ b/.github/workflows/autofix.yml
@@ -8,7 +8,7 @@ permissions:
contents: read
jobs:
- autofix:
+ autofix-changelog-headers:
name: Autofix and style
runs-on: ubuntu-latest
steps:
@@ -24,3 +24,48 @@ jobs:
- uses: j178/prek-action@v2.0.5
- uses: autofix-ci/action@c5b2d67aa2274e7b5a18224e8171550871fc7e4a
if: success() || failure()
+ autofix-dependencies:
+ name: Autoupdate 'old' CI lockfile, README dependencies, and environment file
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7.0.0
+ with:
+ persist-credentials: false
+ - uses: astral-sh/setup-uv@v8.2.0
+ with:
+ version: ">=0.9"
+ activate-environment: true
+ enable-cache: false
+ python-version: "3.12"
+ - name: Update 'old' CI lockfile
+ # uv pip compile requires setting the python version for the environment explicitly in the command :(
+ run: |
+ uv pip compile pyproject.toml \
+ --python "3.10" --python-platform "x86_64-unknown-linux-gnu" \
+ --group test --group lockfile_extras \
+ --resolution lowest-direct \
+ --format pylock.toml --output-file tools/pylock.ci-old.toml \
+ --no-cache
+ - name: Validate 'old' CI lockfile
+ uses: tsbinns/mne-tools/actions/check-lockfile@tools-actions
+ with:
+ project-root: ${{ github.workspace }}
+ lockfile-path: tools/pylock.ci-old.toml
+ groups: lockfile_extras
+ - name: Sync dependencies to README
+ uses: tsbinns/mne-tools/actions/sync-dependencies-to-readme@tools-actions
+ with:
+ project-root: ${{ github.workspace }}
+ ignore-upper-pins: numpy
+ - name: Sync dependencies to environment file
+ uses: tsbinns/mne-tools/actions/sync-dependencies-to-environment-file@tools-actions
+ with:
+ project-root: ${{ github.workspace }}
+ extras: full,full-no-qt,hdf5
+ # manually specify extras groups to sync, as logic for recursion into other groups isn't implemented in the action yet
+ # e.g., `full` includes `mne[full-no-qt]`, but the action won't recognise this, so we need to specify `full-no-qt` explicitly
+ additional-dependencies: pip,mamba,conda,nomkl,noqt5
+ pip-dependencies: pymef
+ requirements-overrides: PySide6==6.11.1,vtk==9.6.2
+ - uses: autofix-ci/action@c5b2d67aa2274e7b5a18224e8171550871fc7e4a
+ if: success() || failure()
diff --git a/.github/workflows/spec_zero.yml b/.github/workflows/spec_zero.yml
index 2e97e6f1d43..63aa8b00768 100644
--- a/.github/workflows/spec_zero.yml
+++ b/.github/workflows/spec_zero.yml
@@ -41,27 +41,12 @@ jobs:
timeout-minutes: 10
with:
detached: true
- - uses: astral-sh/setup-uv@v8.3.2
- with:
- version: ">=0.9"
- activate-environment: true
- python-version: "3.12"
+ - name: Setup Python
+ uses: actions/setup-python@v6.2.0
- name: Install dependencies
- run: uv pip install -e . packaging requests tomlkit prek
+ run: pip install --upgrade packaging requests tomlkit
- name: Update tracked dependencies
run: python ./tools/dev/spec_zero_update_versions.py
- - name: Sync updated dependencies to README
- run: python tools/sync_dependencies.py
- - name: Create lockfile for old CI
- # uv pip compile requires setting the python version explicitly in the command :(
- run: |
- uv pip compile pyproject.toml \
- --python "3.10" --python-platform "x86_64-unknown-linux-gnu" \
- --group test --group lockfile_extras \
- --resolution lowest-direct \
- --format pylock.toml --output-file tools/pylock.ci-old.toml
- python tools/github_actions_check_old_lockfile.py
- - run: prek auto-update
- name: check if files changed
run: |
git diff && git status --porcelain
@@ -69,8 +54,6 @@ jobs:
echo "dirty=true" >> $GITHUB_OUTPUT
fi
id: status
- - uses: j178/prek-action@v2.0.5
- if: steps.status.outputs.dirty == 'true'
- name: Create PR
run: |
set -xeo pipefail
@@ -85,4 +68,4 @@ jobs:
git push origin spec_zero
PR_NUM=$(gh pr create --base main --head spec_zero --title "MAINT: Update dependency specifiers" --body "Created by spec_zero [GitHub action](https://github.com/mne-tools/mne-python/actions/runs/${{ github.run_id }}).
*Adjustments may need to be made to shims in \`mne/fixes.py\` and elswhere in this or another PR. \`make -C tools/dev dep\` is a good starting point for finding potential updates.*")
echo "Opened https://github.com/mne-tools/mne-python/pull/${PR_NUM}" >> $GITHUB_STEP_SUMMARY
- if: steps.status.outputs.dirty == 'true' && (success() || failure())
+ if: steps.status.outputs.dirty == 'true'
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 11c903dcc95..cbbc0f03ecb 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -109,6 +109,7 @@ jobs:
if: startswith(matrix.kind, 'pip') && startswith(matrix.os, 'macos')
# Python (if conda)
- uses: mamba-org/setup-micromamba@v3
+ id: setup-micromamba
with:
environment-file: ${{ env.CONDA_ENV }}
environment-name: mne
@@ -118,6 +119,9 @@ jobs:
-v
if: matrix.kind == 'conda'
timeout-minutes: 20
+ - name: Make conda python available to later steps
+ run: echo "${{ steps.setup-micromamba.outputs.environment-path }}/bin" >> "$GITHUB_PATH" # zizmor: ignore[template-injection]
+ if: matrix.kind == 'conda'
# Python (if old)
- uses: astral-sh/setup-uv@v8.3.2
with:
@@ -130,7 +134,11 @@ jobs:
- run: bash ./tools/github_actions_verify_python.sh "${{ matrix.python }}"
- run: bash ./tools/github_actions_dependencies.sh
timeout-minutes: 10
- - run: python ./tools/github_actions_check_old_env.py
+ - name: Check 'old' CI environment
+ uses: tsbinns/mne-tools/actions/check-environment@tools-actions
+ with:
+ project-root: ${{ github.workspace }}
+ groups: lockfile_extras
if: matrix.kind == 'old'
# Minimal commands on Linux (macOS stalls)
- uses: actions/cache@v6.1.0
@@ -141,8 +149,10 @@ jobs:
- run: bash ./tools/get_minimal_commands.sh
if: startswith(matrix.os, 'ubuntu') && matrix.kind != 'minimal' && matrix.kind != 'old' && matrix.kind != 'pip-ft'
- run: bash ./tools/github_actions_infos.sh
- # Check Qt
- - run: bash ./tools/check_qt_import.sh $MNE_QT_BACKEND
+ - name: Check Qt import
+ uses: tsbinns/mne-tools/actions/check-qt-import@tools-actions
+ with:
+ qt-backend: ${{ env.MNE_QT_BACKEND }}
if: env.MNE_QT_BACKEND != ''
- name: Run tests with no testing data
run: MNE_SKIP_TESTING_DATASET_TESTS=true pytest -m "not (ultraslowtest or pgtest)" -n "$PYTEST_XDIST_N" --dist loadscope --timeout=120 --timeout-method=thread -o faulthandler_timeout=110 $COV_ARGS --tb=short -vv -rfE mne/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 65aea8ac084..58edf88dd13 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -89,15 +89,6 @@ repos:
- id: toml-sort-fix
files: pyproject.toml
- # dependencies
- - repo: local
- hooks:
- - id: update-env-file
- name: Copy dependency changes from pyproject.toml to environment.yml
- language: python
- entry: ./tools/hooks/update_environment_file.py
- files: '^(pyproject.toml|tools/hooks/update_environment_file.py)$'
-
# zizmor
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.28.0
diff --git a/.yamllint.yml b/.yamllint.yml
index 3b8f96c0e53..50100964bfe 100644
--- a/.yamllint.yml
+++ b/.yamllint.yml
@@ -8,3 +8,5 @@ rules:
document-start: disable
new-lines:
type: platform
+ indentation:
+ indent-sequences: consistent
diff --git a/README.rst b/README.rst
index 1daee481884..fae065fb628 100644
--- a/README.rst
+++ b/README.rst
@@ -70,20 +70,21 @@ Dependencies
The minimum required dependencies to run MNE-Python are:
-.. ↓↓↓ BEGIN CORE DEPS LIST. DO NOT EDIT! HANDLED BY PRE-COMMIT HOOK ↓↓↓
-
-- `Python `__ ≥ 3.10
-- `NumPy `__ ≥ 2.0
-- `SciPy `__ ≥ 1.14
-- `Matplotlib `__ ≥ 3.9
-- `Pooch `__ ≥ 1.5
+.. ↓↓↓ BEGIN CORE DEPS LIST. DO NOT EDIT! HANDLED BY MNE-TOOLS ↓↓↓
+
+- `python `__ ≥ 3.10
+- `decorator `__ ≥ 5.1
+- `jinja2 `__ ≥ 3.1
+- `lazy_loader `__ ≥ 0.3
+- `matplotlib `__ ≥ 3.9
+- `numpy `__ ≥ 2.0,
+- `packaging `__
+- `pooch `__ ≥ 1.5
+- `scipy `__ ≥ 1.14
- `tqdm `__ ≥ 4.66
-- `Jinja2 `__ ≥ 3.1
-- `decorator `__ ≥ 5.1
-- `lazy-loader `__ ≥ 0.3
-- `packaging `__
+- `typing-extensions `__ ≥ 4.15
-.. ↑↑↑ END CORE DEPS LIST. DO NOT EDIT! HANDLED BY PRE-COMMIT HOOK ↑↑↑
+.. ↑↑↑ END CORE DEPS LIST. DO NOT EDIT! HANDLED BY MNE-TOOLS ↑↑↑
Contributing
^^^^^^^^^^^^
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index a201c291ea2..0f151e5d05b 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -42,12 +42,17 @@ stages:
pool:
vmImage: 'ubuntu-latest'
variables:
+ MNE_TOOLS_DIR: '$(Pipeline.Workspace)/mne-tools'
DISPLAY: ':99'
PYTEST_XDIST_N: '2' # Microsoft-hosted agents have 2 cores
MNE_TEST_ALLOW_SKIP: '^$' # nothing
MNE_BROWSER_PRECOMPUTE: 'false'
steps:
- - bash: ./tools/setup_xvfb.sh
+ - bash: |
+ set -xeo pipefail
+ git clone https://github.com/mne-tools/mne-tools.git "$MNE_TOOLS_DIR"
+ displayName: Clone mne-tools
+ - bash: bash $(MNE_TOOLS_DIR)/tools/setup_xvfb.sh
displayName: Setup up Xvfb
- task: Cache@2
inputs:
@@ -66,7 +71,7 @@ stages:
python -m pip install --progress-bar off --upgrade pip
python -m pip install --progress-bar off --upgrade --only-binary=":all:" -e . --group=test "mne-qt-browser @ https://github.com/mne-tools/mne-qt-browser/archive/refs/heads/main.zip" pyvista scikit-learn python-picard qtpy nibabel sphinx-gallery "PySide6!=6.8.0,!=6.8.0.1,!=6.8.1.1,!=6.9.1" pandas neo pymatreader antio defusedxml curryreader pymef openmeeg
displayName: Install dependencies with pip
- - bash: ./tools/check_qt_import.sh PySide6
+ - bash: bash $(MNE_TOOLS_DIR)/tools/check_qt_import.sh PySide6
displayName: Check Qt import
- bash: |
set -xeo pipefail
@@ -95,12 +100,17 @@ stages:
pool:
vmImage: 'ubuntu-latest'
variables:
+ MNE_TOOLS_DIR: '$(Pipeline.Workspace)/mne-tools'
DISPLAY: ':99'
PYTEST_XDIST_N: '2' # Microsoft-hosted agents have 2 cores
TEST_OPTIONS: "--tb=short --cov=mne --cov-report=xml --cov-append -vv mne/gui mne/viz/_brain mne/viz/backends mne/viz/tests/test_evoked.py mne/report"
MNE_TEST_ALLOW_SKIP: '^$' # nothing (can be overridden below)
steps:
- - bash: ./tools/setup_xvfb.sh
+ - bash: |
+ set -xeo pipefail
+ git clone https://github.com/mne-tools/mne-tools.git "$MNE_TOOLS_DIR"
+ displayName: Clone mne-tools
+ - bash: bash $(MNE_TOOLS_DIR)/tools/setup_xvfb.sh
displayName: Setup up Xvfb
- task: UsePythonVersion@0
inputs:
@@ -128,7 +138,7 @@ stages:
displayName: Cache testing data
- bash: ./tools/github_actions_download.sh
displayName: Download testing data
- - bash: ./tools/check_qt_import.sh PySide6
+ - bash: bash $(MNE_TOOLS_DIR)/tools/check_qt_import.sh PySide6
displayName: Check Qt import
- bash: |
set -xeo pipefail
diff --git a/environment.yml b/environment.yml
index 8a30aa78030..ed8e42a6e07 100644
--- a/environment.yml
+++ b/environment.yml
@@ -1,4 +1,4 @@
-# THIS FILE IS AUTO-GENERATED BY tools/hooks/update_environment_file.py AND WILL BE OVERWRITTEN
+# THIS FILE IS AUTO-GENERATED BY MNE-TOOLS AND WILL BE OVERWRITTEN
name: mne
channels:
- conda-forge
@@ -13,7 +13,6 @@ dependencies:
- dipy >=0.8
- edfio >=0.4.10
- eeglabio
- - ffmpeg =8.1.2
- filelock >=3.18.0
- h5io >=0.2.4
- h5py >=2.4
@@ -21,7 +20,7 @@ dependencies:
- imageio-ffmpeg >=0.4.1
- ipyevents
- ipympl
- - ipython >=2.0,!=8.7.0
+ - ipython !=8.7.0,>=2.0
- ipywidgets
- jinja2 >=3.1
- joblib >=0.8
@@ -37,10 +36,10 @@ dependencies:
- nomkl
- noqt5
- numba >=0.35
- - numpy >=2.0,<3
+ - numpy <3,>=2.0
- openmeeg >=2.5.7
- packaging
- - pandas >=2.2,!=3.0.4
+ - pandas !=3.0.4,>=2.2
- pillow
- pip
- pooch >=1.5
@@ -69,5 +68,6 @@ dependencies:
- vtk ==9.6.2
- xlrd
- pip:
+ - typing-extensions>=4.15; python_version < "3.11"
- pymef
- - pyobjc-framework-Cocoa >=5.2.0;platform_system=='Darwin'
+ - pyobjc-framework-Cocoa>=5.2.0; platform_system == "Darwin"
diff --git a/pyproject.toml b/pyproject.toml
index 22af8942ad5..63996766cbd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -96,6 +96,7 @@ classifiers = [
"License :: OSI Approved",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
+ "Operating System :: POSIX :: Linux",
"Operating System :: POSIX",
"Operating System :: Unix",
"Programming Language :: Python :: 3",
@@ -115,13 +116,21 @@ dependencies = [
"tqdm >= 4.66",
"typing-extensions >= 4.15; python_version < '3.11'", # for typing.Self TODO VERSION
]
-description = "MNE-Python project for MEG and EEG data analysis."
+description = "MNE-Python is an open-source Python package for exploring, visualizing, and analyzing human neurophysiological data. It provides methods for data input/output, preprocessing, visualization, source estimation, time-frequency analysis, machine learning, and statistics."
dynamic = ["version"]
keywords = [
"brain",
+ "DBS",
+ "deep brain stimulation",
"ECoG",
"EEG",
+ "electrocorticography",
+ "electroencephalography",
"fNIRS",
+ "functional near-infrared spectroscopy",
+ "iEEG",
+ "intracranial EEG",
+ "magnetoencephalography",
"MEG",
"neuroimaging",
"neuroscience",
@@ -133,7 +142,7 @@ name = "mne"
readme = {content-type = "text/x-rst", file = "README.rst"}
requires-python = ">= 3.10"
# ↑↑↑↑↑↑↑↑↑↑↑ when this changes, bump the `--python-version` in the `uv pip compile ...`
-# command in `.github/workflows/spec_zero.yaml` (astral-sh/uv/#16333), and also the
+# command in `.github/workflows/autofix.yml` (astral-sh/uv/#16333), and also the
# `python` version for the `kind: old` matrix entry in `.github/workflows/tests.yaml`
scripts = {mne = "mne.commands.utils:main"}
diff --git a/tools/check_qt_import.sh b/tools/check_qt_import.sh
deleted file mode 100755
index fa128e7f267..00000000000
--- a/tools/check_qt_import.sh
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/bin/bash -ef
-
-set -eo pipefail
-
-if [[ "$1" == "" ]]; then
- echo "Qt library must be provided as first argument"
- exit 1
-fi
-echo "Checking if Python-Qt binding $1 is available"
-echo ""
-set -x
-LD_DEBUG=libs python -c "from $1.QtWidgets import QApplication, QWidget; app = QApplication([])"
-set +x
-echo ""
-echo "Python-Qt binding $1 is available!"
diff --git a/tools/circleci_bash_env.sh b/tools/circleci_bash_env.sh
index 55179e97f4f..e5b372d9db8 100755
--- a/tools/circleci_bash_env.sh
+++ b/tools/circleci_bash_env.sh
@@ -3,7 +3,8 @@
set -e
set -o pipefail
-./tools/setup_xvfb.sh
+curl https://raw.githubusercontent.com/mne-tools/mne-tools/main/tools/setup_xvfb.sh -o setup_xvfb.sh
+bash setup_xvfb.sh
# Need different installs for 24.04 and 26.04
if [[ $(lsb_release -rs) == "26.04" ]]; then
EXTRA_DEPS="libgvplugin-neato-layout8"
diff --git a/tools/generate_codemeta.py b/tools/generate_codemeta.py
deleted file mode 100644
index a82a521e067..00000000000
--- a/tools/generate_codemeta.py
+++ /dev/null
@@ -1,250 +0,0 @@
-# Authors: The MNE-Python contributors.
-# License: BSD-3-Clause
-# Copyright the MNE-Python contributors.
-
-import subprocess
-from argparse import ArgumentParser
-from datetime import date
-from pathlib import Path
-
-import tomllib
-
-parser = ArgumentParser(description="Generate codemeta.json and CITATION.cff")
-parser.add_argument("release_version", type=str)
-release_version = parser.parse_args().release_version
-
-out_dir = Path(__file__).parents[1]
-
-# NOTE: ../codemeta.json and ../citation.cff should not be continuously
-# updated. Run this script only at release time.
-
-package_name = "MNE-Python"
-release_date = str(date.today())
-commit = subprocess.run(
- ["git", "log", "-1", "--pretty=%H"], capture_output=True, text=True
-).stdout.strip()
-
-# KEYWORDS
-keywords = (
- "MEG",
- "magnetoencephalography",
- "EEG",
- "electroencephalography",
- "fNIRS",
- "functional near-infrared spectroscopy",
- "iEEG",
- "intracranial EEG",
- "eCoG",
- "electrocorticography",
- "DBS",
- "deep brain stimulation",
-)
-
-# add to these as necessary
-compound_surnames = (
- "García Alanis",
- "van Vliet",
- "De Santis",
- "Dupré la Tour",
- "de la Torre",
- "de Jong",
- "de Montalivet",
- "van den Bosch",
- "Van den Bossche",
- "Van Der Donckt",
- "van der Meer",
- "van Harmelen",
- "Visconti di Oleggio Castello",
- "van Es",
-)
-
-
-def parse_name(name):
- """Split name blobs from `git shortlog -nse` into first/last/email."""
- # remove commit count
- _, name_and_email = name.strip().split("\t")
- name, email = name_and_email.split(" <")
- email = email.strip(">")
- email = "" if "noreply" in email else email # ignore "noreply" emails
- name = " ".join(name.split(".")) # remove periods from initials
- # handle compound surnames
- for compound_surname in compound_surnames:
- if name.endswith(compound_surname):
- ix = name.index(compound_surname)
- first = name[:ix].strip()
- last = compound_surname
- return (first, last, email)
- # handle non-compound surnames
- name_elements = name.split()
- if len(name_elements) == 1: # mononyms / usernames
- first = ""
- last = name
- else:
- first = " ".join(name_elements[:-1])
- last = name_elements[-1]
- return (first, last, email)
-
-
-# MAKE SURE THE RELEASE STRING IS PROPERLY FORMATTED
-try:
- split_version = list(map(int, release_version.split(".")))
-except ValueError:
- raise
-msg = (
- "First argument must be the release version X.Y.Z (all integers), "
- f"got {release_version}"
-)
-assert len(split_version) == 3, msg
-
-
-# RUN GIT SHORTLOG TO GET ALL AUTHORS, SORTED BY NUMBER OF COMMITS
-args = ["git", "shortlog", "-nse"]
-result = subprocess.run(args, capture_output=True, text=True)
-lines = result.stdout.strip().split("\n")
-all_names = [parse_name(line) for line in lines if "[bot]" not in line]
-
-
-# CONSTRUCT JSON AUTHORS LIST
-json_authors = [
- f"""{{
- "@type":"Person",
- "email":"{email}",
- "givenName":"{first}",
- "familyName": "{last}"
- }}"""
- for (first, last, email) in all_names
-]
-
-
-# GET OUR DEPENDENCY VERSIONS
-pyproject = tomllib.loads(
- (Path(__file__).parents[1] / "pyproject.toml").read_text("utf-8")
-)
-dependencies = [f"python{pyproject['project']['requires-python']}"]
-dependencies.extend(pyproject["project"]["dependencies"])
-
-# these must be done outside the boilerplate (no \n allowed in f-strings):
-json_authors = ",\n ".join(json_authors)
-dependencies = '",\n "'.join(dependencies)
-json_keywords = '",\n "'.join(keywords)
-
-
-# ASSEMBLE COMPLETE JSON
-codemeta_boilerplate = f"""{{
- "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
- "@type": "SoftwareSourceCode",
- "license": "https://spdx.org/licenses/BSD-3-Clause",
- "codeRepository": "git+https://github.com/mne-tools/mne-python.git",
- "dateCreated": "2010-12-26",
- "datePublished": "2014-08-04",
- "dateModified": "{release_date}",
- "downloadUrl": "https://github.com/mne-tools/mne-python/archive/v{release_version}.zip",
- "issueTracker": "https://github.com/mne-tools/mne-python/issues",
- "name": "{package_name}",
- "version": "{release_version}",
- "description": "{package_name} is an open-source Python package for exploring, visualizing, and analyzing human neurophysiological data. It provides methods for data input/output, preprocessing, visualization, source estimation, time-frequency analysis, connectivity analysis, machine learning, and statistics.",
- "applicationCategory": "Neuroscience",
- "developmentStatus": "active",
- "referencePublication": "https://doi.org/10.3389/fnins.2013.00267",
- "keywords": [
- "{json_keywords}"
- ],
- "programmingLanguage": [
- "Python"
- ],
- "operatingSystem": [
- "Linux",
- "Windows",
- "macOS"
- ],
- "softwareRequirements": [
- "{dependencies}"
- ],
- "author": [
- {json_authors}
- ]
-}}
-""" # noqa E501
-
-
-# WRITE TO FILE
-with open(out_dir / "codemeta.json", "w") as codemeta_file:
- codemeta_file.write(codemeta_boilerplate)
-
-
-# # # # # # # # # # # # # # #
-# GENERATE CITATION.CFF TOO #
-# # # # # # # # # # # # # # #
-message = (
- "If you use this software, please cite both the software itself, "
- "and the paper listed in the preferred-citation field."
-)
-
-# in CFF, multi-word keywords need to be wrapped in quotes
-cff_keywords = (f'"{kw}"' if " " in kw else kw for kw in keywords)
-# make into a bulleted list
-cff_keywords = "\n".join(f" - {kw}" for kw in cff_keywords)
-
-# TODO: someday would be nice to include ORCiD identifiers too
-cff_authors = [
- f" - family-names: {last}\n given-names: {first}"
- if first
- else f" - name: {last}"
- for (first, last, _) in all_names
-]
-cff_authors = "\n".join(cff_authors)
-
-# this ↓↓↓ is the meta-DOI that always resolves to the latest release
-zenodo_doi = "10.5281/zenodo.592483"
-
-# ASSEMBLE THE CFF STRING
-cff_boilerplate = f"""\
-cff-version: 1.2.0
-title: "{package_name}"
-message: "{message}"
-version: {release_version}
-date-released: "{release_date}"
-commit: {commit}
-doi: {zenodo_doi}
-keywords:
-{cff_keywords}
-authors:
-{cff_authors}
-preferred-citation:
- title: "MEG and EEG Data Analysis with MNE-Python"
- journal: "Frontiers in Neuroscience"
- type: article
- year: 2013
- volume: 7
- issue: 267
- start: 1
- end: 13
- doi: 10.3389/fnins.2013.00267
- authors:
- - family-names: Gramfort
- given-names: Alexandre
- - family-names: Luessi
- given-names: Martin
- - family-names: Larson
- given-names: Eric
- - family-names: Engemann
- given-names: Denis A.
- - family-names: Strohmeier
- given-names: Daniel
- - family-names: Brodbeck
- given-names: Christian
- - family-names: Goj
- given-names: Roman
- - family-names: Jas
- given-names: Mainak
- - family-names: Brooks
- given-names: Teon
- - family-names: Parkkonen
- given-names: Lauri
- - family-names: Hämäläinen
- given-names: Matti S.
-"""
-
-# WRITE TO FILE
-with open(out_dir / "CITATION.cff", "w") as cff_file:
- cff_file.write(cff_boilerplate)
diff --git a/tools/github_actions_check_old_env.py b/tools/github_actions_check_old_env.py
deleted file mode 100644
index 7ed0e1bd4d0..00000000000
--- a/tools/github_actions_check_old_env.py
+++ /dev/null
@@ -1,80 +0,0 @@
-"""Check that the old env being used has the expected versions of dependencies."""
-
-# Authors: The MNE-Python contributors.
-# License: BSD-3-Clause
-# Copyright the MNE-Python contributors.
-
-import importlib
-import sys
-from importlib import metadata
-from pathlib import Path
-
-from packaging.version import Version
-
-project_root = Path(__file__).parent.parent
-
-sys.path.append(project_root / "tools")
-from check_pyproject_helpers import ( # noqa: E402
- get_bad_deps_message,
- get_deps_to_check,
- get_min_pinned_ver,
- raise_bad_deps_messages,
-)
-
-# Get dependencies to check from pyproject.toml
-check_deps = get_deps_to_check()
-
-# Check that the versions in the env match the minimum versions in pyproject.toml
-mod_name_mapping = {"scikit-learn": "sklearn"}
-bad_missing = []
-bad_version = []
-for dep in check_deps:
- mod_name, pyproject_ver = get_min_pinned_ver(dep)
- mod_import_name = mod_name_mapping.get(mod_name, mod_name).replace("-", "_")
-
- # Be wary of uv treating lowest Python vs. module versions differently.
- # For Python, the latest micro version for the major.minor release specified will be
- # used. E.g., if we ask for 3.10 when creating the old env, we will get 3.10.19.
- # However, for modules, uv's `lowest-direct` option will resolve to the lowest
- # major.minor.micro version, even if a micro version isn't specified. E.g., if
- # `pyproject.toml` asks for numpy >= 1.26, the lockfile will have 1.26.0.
- # However, the non-lowest micro version of a module may be selected for
- # compatibility reasons. E.g., if `pyproject.toml` asks for pandas >= 2.2 and numpy
- # >= 2.0, pandas 2.2.2 will be placed in the lockfile, as that was the first version
- # to support numpy 2.0. Non-lowest micro versions of modules may also not be used if
- # they were yanked.
- # Therefore, if the micro version of a module isn't specified in `pyproject.toml`,
- # we don't check the micro version of the module in the environment.
- if mod_name == "python":
- env_ver = sys.version_info[:3] # take major, minor, and micro info
- env_ver = ".".join(str(x) for x in env_ver)
- else:
- try:
- importlib.import_module(mod_import_name)
- except Exception as exc:
- bad_missing.append(f"{mod_name}: ({type(exc).__name__}: {exc})")
- continue
- # Not all modules have a __version__ attribute, so use importlib.metadata
- # Also requires the true module name, not the import variant (if different)
- env_ver = metadata.version(mod_name)
-
- if pyproject_ver is None:
- continue # no min version specified, so no check needed
- pyproject_ver = Version(pyproject_ver)
- env_ver = Version(env_ver)
-
- # Discard micro info from env version if it's not specified in pyproject.toml
- if len(pyproject_ver.release) == 2:
- env_ver = Version(f"{env_ver.major}.{env_ver.minor}")
-
- if env_ver != pyproject_ver:
- bad_version.append(
- f"{mod_name}: is {env_ver}; {pyproject_ver} expected from `pyproject.toml`"
- )
-
-# Format bad messages and raise if there are any bads
-bad_missing = get_bad_deps_message(bad_missing, "are missing from the environment")
-bad_version = get_bad_deps_message(
- bad_version, "have incorrect versions in the environment"
-)
-raise_bad_deps_messages([bad_missing, bad_version])
diff --git a/tools/github_actions_check_old_lockfile.py b/tools/github_actions_check_old_lockfile.py
deleted file mode 100644
index 5fbf9dfbc08..00000000000
--- a/tools/github_actions_check_old_lockfile.py
+++ /dev/null
@@ -1,84 +0,0 @@
-"""Check that the old env lockfile has the expected versions of dependencies."""
-
-# Authors: The MNE-Python contributors.
-# License: BSD-3-Clause
-# Copyright the MNE-Python contributors.
-
-import sys
-from pathlib import Path
-
-from packaging.specifiers import Specifier
-from packaging.version import Version
-from tomlkit.toml_file import TOMLFile
-
-project_root = Path(__file__).parent.parent
-
-sys.path.append(project_root / "tools")
-from check_pyproject_helpers import ( # noqa: E402
- get_bad_deps_message,
- get_deps_to_check,
- get_min_pinned_ver,
- raise_bad_deps_messages,
-)
-
-# Get dependencies to check from pyproject.toml
-check_deps = get_deps_to_check()
-
-# Get 'old' lockfile pins for dependencies
-lockfile = TOMLFile(project_root / "tools/pylock.ci-old.toml")
-lockfile_data = lockfile.read()
-python_spec = Specifier(lockfile_data["requires-python"])
-assert python_spec.operator == ">=", (
- f"Expected the Python version specifier in `pylock.ci-old.toml` to be a '>=' "
- f"specifier, but found {python_spec.operator}."
-)
-lockfile_modules = {"python": python_spec.version}
-lockfile_modules.update(
- {mod["name"]: mod["version"] for mod in lockfile_data["packages"]}
-)
-
-# Check that the versions in the lockfile match the minimum versions in pyproject.toml
-# For modules, uv's `lowest-direct` option will resolve to the lowest
-# major.minor.micro version, even if a micro version isn't specified. E.g., if
-# `pyproject.toml` asks for numpy >= 1.26, the lockfile will have 1.26.0.
-# However, the non-lowest micro version of a module may be selected for
-# compatibility reasons. E.g., if `pyproject.toml` asks for pandas >= 2.2 and numpy
-# >= 2.0, pandas 2.2.2 will be placed in the lockfile, as that was the first version
-# to support numpy 2.0. Non-lowest micro versions of modules may also not be used if
-# they were yanked.
-# Therefore, if the micro version of a module isn't specified in `pyproject.toml`,
-# we don't check the micro version of the module in the environment.
-mod_name_mapping = {"lazy_loader": "lazy-loader"}
-bad_missing = []
-bad_version = []
-for dep in check_deps:
- mod_name, pyproject_ver = get_min_pinned_ver(dep)
- if pyproject_ver is None:
- continue # no min version specified, so no check needed
- pyproject_ver = Version(pyproject_ver)
- name = mod_name_mapping.get(mod_name, mod_name)
-
- if name not in lockfile_modules.keys():
- bad_missing.append(name)
- continue
- lockfile_ver = lockfile_modules[name]
- lockfile_ver = Version(lockfile_ver)
-
- # Discard micro info from lockfile version if it's not specified in pyproject.toml
- if len(pyproject_ver.release) == 2:
- lockfile_ver = Version(f"{lockfile_ver.major}.{lockfile_ver.minor}")
-
- if lockfile_ver != pyproject_ver:
- bad_version.append(
- f"lower pin on {name} in `pyproject.toml` is {pyproject_ver}, "
- f"but `pylock.ci-old.toml` has {lockfile_ver}"
- )
-
-# Format bad messages and raise if there are any bads
-bad_missing = get_bad_deps_message(
- bad_missing, "are missing from the `pylock.ci-old.toml` lockfile"
-)
-bad_version = get_bad_deps_message(
- bad_version, "have incorrect versions in the `pylock.ci-old.toml` lockfile"
-)
-raise_bad_deps_messages([bad_missing, bad_version])
diff --git a/tools/github_actions_dependencies.sh b/tools/github_actions_dependencies.sh
index a48ae02b433..13fee17afa3 100755
--- a/tools/github_actions_dependencies.sh
+++ b/tools/github_actions_dependencies.sh
@@ -29,7 +29,7 @@ elif [[ "${MNE_CI_KIND}" == "old" ]]; then
STD_ARGS="--progress-bar off"
echo "::group::Syncing old environment dependencies from lockfile using uv"
uv pip sync ${SCRIPT_DIR}/pylock.ci-old.toml
- uv pip install pip tomlkit ${MNE_QT_BACKEND}
+ uv pip install pip ${MNE_QT_BACKEND}
echo "::endgroup::"
elif [[ "${MNE_CI_KIND}" == "pip-ft" ]]; then
# This one is free-threaded so can't have PySide6/PyQt6 as of 2026/06/16
diff --git a/tools/hooks/update_environment_file.py b/tools/hooks/update_environment_file.py
deleted file mode 100755
index 9c858ca70ad..00000000000
--- a/tools/hooks/update_environment_file.py
+++ /dev/null
@@ -1,101 +0,0 @@
-#!/usr/bin/env python
-
-# Authors: The MNE-Python contributors.
-# License: BSD-3-Clause
-# Copyright the MNE-Python contributors.
-
-import re
-from pathlib import Path
-
-import tomllib
-
-repo_root = Path(__file__).resolve().parents[2]
-with open(repo_root / "pyproject.toml", "rb") as fid:
- pyproj = tomllib.load(fid)
-
-# Get our "full" dependences from `pyproject.toml`, but actually ignore the
-# "full-pyqt6" section as it's just "full-noqt" plus PyQt6. Also ignore "full-pyside6"
-# as it's just a redirect to "full".
-ignore = ("full-pyqt6", "full-pyside6")
-deps = set(pyproj["project"]["dependencies"])
-for section, section_deps in pyproj["project"]["optional-dependencies"].items():
- if section not in ignore:
- deps |= set(section_deps)
-recursive_deps = set(d for d in deps if d.startswith("mne["))
-deps -= recursive_deps
-deps |= {"pip", "mamba", "conda", "nomkl", "noqt5"}
-# not on conda-forge
-pip_deps = {
- "pymef", # not on conda-forge
-}
-deps -= pip_deps
-deps -= {
- "typing-extensions >= 4.15; python_version < '3.11'"
-} # for typing.Self TODO VERSION
-
-
-def remove_spaces(version_spec):
- """Remove spaces in version specs (conda is stricter than pip about this).
-
- https://docs.conda.io/projects/conda/en/latest/user-guide/concepts/pkg-specs.html#package-match-specifications
- """
- return "".join(version_spec.split())
-
-
-def split_dep(dep):
- """Separate package name from version spec."""
- pattern = re.compile(r"([^!=<>]+)?([!=<>].*)?")
- groups = list(pattern.match(dep).groups())
- groups[1] = "" if groups[1] is None else remove_spaces(groups[1])
- return tuple(map(str.strip, groups))
-
-
-# python version
-req_python = remove_spaces(pyproj["project"]["requires-python"])
-
-# split package name from version spec
-translations = dict(neo="python-neo")
-conda_dep_lines = set()
-version_spec_overrides = {
- # Help the solver work faster by specifying these (should be updated periodically):
- "PySide6": "==6.11.1",
- "vtk": "==9.6.2",
-}
-for key in version_spec_overrides:
- assert any(dep.startswith(key) for dep in deps), (
- f"Need to adjust code below if {key} is not a dependency: {deps}"
- )
-for dep in deps:
- package_name, version_spec = split_dep(dep)
- version_spec = version_spec_overrides.get(package_name, version_spec)
- # handle package name differences
- package_name = translations.get(package_name, package_name)
- # C deps that mean we need to upgrade VTK etc.
- # rstrip output line in case `version_spec` == ""
- line = f" - {package_name} {version_spec}".rstrip()
- # use pip for packages needing e.g. `platform_system` or `python_version` triaging
- if ";" in version_spec:
- pip_deps.add(line[4:])
- elif package_name not in pip_deps:
- conda_dep_lines.add(line)
-conda_dep_lines.add(" - ffmpeg =8.1.2") # try to fix conda issue
-
-# prepare the pip dependencies section
-newline = "\n" # python < 3.12 forbids backslash in {} part of f-string
-pip_section = f"""\
- - pip:
-{newline.join(sorted((f" - {dep}" for dep in pip_deps), key=str.casefold))}
-"""
-pip_section = pip_section if len(pip_deps) else ""
-# prepare the env file
-env = f"""\
-# THIS FILE IS AUTO-GENERATED BY {"/".join(Path(__file__).parts[-3:])} AND WILL BE OVERWRITTEN
-name: mne
-channels:
- - conda-forge
-dependencies:
- - python {req_python}
-{newline.join(sorted(conda_dep_lines, key=str.casefold))}
-{pip_section}""" # noqa: E501
-
-(repo_root / "environment.yml").write_text(env)
diff --git a/tools/install_pre_requirements.sh b/tools/install_pre_requirements.sh
index 8c56b85e2e7..f8402e3fac9 100755
--- a/tools/install_pre_requirements.sh
+++ b/tools/install_pre_requirements.sh
@@ -2,7 +2,6 @@
set -eo pipefail
-SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
PLATFORM=$(python -c 'import platform; print(platform.system())')
echo "Installing pip-pre dependencies on ${PLATFORM}"
@@ -78,5 +77,6 @@ python -c "import numpy as np; assert np.__version__[0] == '2', np.__version__"
echo "::endgroup::"
echo "::group::Check Qt import"
-${SCRIPT_DIR}/check_qt_import.sh "$MNE_QT_BACKEND"
+curl https://raw.githubusercontent.com/mne-tools/mne-tools/main/tools/check_qt_import.sh -o check_qt_import.sh
+bash check_qt_import.sh "$MNE_QT_BACKEND"
echo "::endgroup::"
diff --git a/tools/pylock.ci-old.toml b/tools/pylock.ci-old.toml
index 8e020000522..b54618a909b 100644
--- a/tools/pylock.ci-old.toml
+++ b/tools/pylock.ci-old.toml
@@ -1,5 +1,5 @@
# This file was autogenerated by uv via the following command:
-# uv pip compile pyproject.toml --python 3.10 --python-platform x86_64-unknown-linux-gnu --group test --group lockfile_extras --resolution lowest-direct --format pylock.toml --output-file tools/pylock.ci-old.toml
+# uv pip compile pyproject.toml --python 3.10 --python-platform x86_64-unknown-linux-gnu --group test --group lockfile_extras --resolution lowest-direct --format pylock.toml --output-file tools/pylock.ci-old.toml --no-cache
lock-version = "1.0"
created-by = "uv"
requires-python = ">=3.10"
diff --git a/tools/setup_xvfb.sh b/tools/setup_xvfb.sh
deleted file mode 100755
index a48e5703197..00000000000
--- a/tools/setup_xvfb.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/bin/bash -ef
-
-# this is only relevant for GitHub Actions, but it avoids
-# https://github.com/actions/virtual-environments/issues/323
-# via
-# https://github.community/t/ubuntu-latest-apt-repository-list-issues/17182/10#M4501
-for apt_file in `grep -lr microsoft /etc/apt/sources.list.d/`; do
- echo "Removing $apt_file"
- sudo rm $apt_file
-done
-
-# This also includes the libraries necessary for Qt6
-sudo apt update
-# Need two different libxml2 variants for 24.04 and 26.04
-if [[ $(lsb_release -rs) == "26.04" ]]; then
- XML_DEP=libxml2-16
-else
- XML_DEP=libxml2
-fi
-sudo apt install -yqq xvfb libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 libopengl0 libegl1 libosmesa6 mesa-utils libxcb-shape0 libxcb-cursor0 $XML_DEP
-/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render -noreset
diff --git a/tools/sync_dependencies.py b/tools/sync_dependencies.py
deleted file mode 100755
index e720d58e1cb..00000000000
--- a/tools/sync_dependencies.py
+++ /dev/null
@@ -1,108 +0,0 @@
-#!/usr/bin/env python
-
-# Authors: The MNE-Python contributors.
-# License: BSD-3-Clause
-# Copyright the MNE-Python contributors.
-
-import difflib
-import re
-
-# NB here we use metadata from the latest stable release because this goes in our
-# README, which should apply to the latest release (rather than dev).
-# For oldest supported dev dependencies, see update_environment_file.py.
-from importlib.metadata import metadata
-from pathlib import Path
-
-from mne.utils import _pl, warn
-
-README_PATH = Path(__file__).parents[1] / "README.rst"
-BEGIN = ".. ↓↓↓ BEGIN CORE DEPS LIST. DO NOT EDIT! HANDLED BY PRE-COMMIT HOOK ↓↓↓"
-END = ".. ↑↑↑ END CORE DEPS LIST. DO NOT EDIT! HANDLED BY PRE-COMMIT HOOK ↑↑↑"
-
-CORE_DEPS_URLS = {
- "Python": "https://www.python.org",
- "NumPy": "https://numpy.org",
- "SciPy": "https://scipy.org",
- "Matplotlib": "https://matplotlib.org",
- "Pooch": "https://www.fatiando.org/pooch/latest/",
- "tqdm": "https://tqdm.github.io",
- "Jinja2": "https://palletsprojects.com/p/jinja/",
- "decorator": "https://github.com/micheles/decorator",
- "lazy-loader": "https://pypi.org/project/lazy_loader",
- "packaging": "https://packaging.pypa.io/en/stable/",
-}
-IGNORED_CORE_DEPS = ["typing-extensions"]
-
-
-def _prettify_pin(pin):
- if pin is None:
- return ""
- pins = pin.split(",")
- replacements = {
- "<=": " ≤ ",
- ">=": " ≥ ",
- "<": " < ",
- ">": " > ",
- }
- for old, new in replacements.items():
- pins = [p.replace(old, new) for p in pins]
- pins = reversed(pins)
- return ",".join(pins)
-
-
-# get the dependency info
-py_pin = metadata("mne").get("Requires-Python")
-all_deps = metadata("mne").get_all("Requires-Dist")
-core_deps = [f"python{py_pin}", *[dep for dep in all_deps if "extra ==" not in dep]]
-pattern = re.compile(r"(?P[A-Za-z_\-\d]+)(?P[<>=]+.*)?")
-core_deps_pins = {
- dep["name"]: _prettify_pin(dep["pin"]) for dep in map(pattern.match, core_deps)
-}
-# remove ignored deps
-for dep in IGNORED_CORE_DEPS:
- core_deps_pins.pop(dep, None)
-# don't show upper pin on NumPy (not important for users, just devs)
-new_pin = core_deps_pins["numpy"].split(",")
-new_pin.remove(" < 3")
-core_deps_pins["numpy"] = new_pin[0]
-
-# make sure our URLs dict is minimal and complete
-missing_urls = set(core_deps_pins) - {dep.lower() for dep in CORE_DEPS_URLS}
-extra_urls = {dep.lower() for dep in CORE_DEPS_URLS} - set(core_deps_pins)
-update_msg = (
- "please update `CORE_DEPS_URLS` mapping in `tools/hooks/sync_dependencies.py`."
-)
-if missing_urls:
- _s = _pl(missing_urls)
- raise RuntimeError(
- f"Missing URL{_s} for package{_s} {', '.join(missing_urls)}; {update_msg}"
- )
-if extra_urls:
- _s = _pl(extra_urls)
- warn(f"Superfluous URL{_s} for package{_s} {', '.join(extra_urls)}; {update_msg}")
-
-# construct the rST
-core_deps_bullets = [
- f"- `{key} <{url}>`__{core_deps_pins[key.lower()]}"
- for key, url in CORE_DEPS_URLS.items()
-]
-
-# rewrite the README file
-lines = README_PATH.read_text("utf-8").splitlines()
-out_lines = list()
-skip = False
-for line in lines:
- if line.strip() == BEGIN:
- skip = True
- out_lines.append(line)
- out_lines.extend(["", *core_deps_bullets, ""])
- if line.strip() == END:
- skip = False
- if not skip:
- out_lines.append(line)
-new = "\n".join(out_lines) + "\n"
-old = README_PATH.read_text("utf-8")
-if new != old:
- diff = "\n".join(difflib.unified_diff(old.splitlines(), new.splitlines()))
- print(f"Updating {README_PATH} with diff:\n{diff}")
- README_PATH.write_text(new, encoding="utf-8")