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")