Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/autofix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ permissions:

jobs:
autofix:
name: Autoupdate changelog entry and headers
name: Autoupdate pre-commit, changelog, and headers
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.0
Expand All @@ -19,4 +19,7 @@ jobs:
- run: pip install --upgrade towncrier pygithub gitpython numpy
- run: python ./.github/actions/rename_towncrier/rename_towncrier.py
- run: python ./tools/dev/ensure_headers.py
# Run this last, since it can fail the job if moved up
- uses: j178/prek-action@v2
- uses: autofix-ci/action@c5b2d67aa2274e7b5a18224e8171550871fc7e4a
if: success() || failure()
2 changes: 1 addition & 1 deletion .github/workflows/automerge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ permissions:
jobs:
autobot:
runs-on: ubuntu-latest
if: (github.event.pull_request.user.login == 'dependabot[bot]' || github.event.pull_request.user.login == 'pre-commit-ci[bot]') && github.repository == 'mne-tools/mne-python'
if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'mne-tools/mne-python'
steps:
- name: Enable auto-merge for bot PRs
run: gh pr merge --auto --squash "$PR_URL"
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/autopush.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ on: # yamllint disable-line rule:truthy
push:
branches:
- dependabot/**
- pre-commit-ci*

jobs:
autobot:
Expand All @@ -23,8 +22,8 @@ jobs:
git config --global user.name "mne[bot]"
git config --global user.email "50266005+mne-bot@users.noreply.github.com"
COMMIT_MESSAGE=$(git show -s --format=%s)
# Detect dependabot, pre-commit.ci, and lumberbot commit messages
if [[ "$COMMIT_MESSAGE" == '[dependabot]'* ]] || [[ "$COMMIT_MESSAGE" == '[pre-commit.ci]'* ]] || [[ "$COMMIT_MESSAGE" == 'Backport PR mne-tools'* ]]; then
# Detect dependabot and lumberbot commit messages
if [[ "$COMMIT_MESSAGE" == '[dependabot]'* ]] || [[ "$COMMIT_MESSAGE" == 'Backport PR mne-tools'* ]]; then
echo "Pushed commit to run CircleCI for: $COMMIT_MESSAGE" | tee -a $GITHUB_STEP_SUMMARY
git commit --allow-empty -m "mne[bot] Push commit to run CircleCI"
git push
Expand Down
16 changes: 9 additions & 7 deletions .github/workflows/spec_zero.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,27 +47,29 @@ jobs:
activate-environment: true
python-version: "3.12"
- name: Install dependencies
run: uv pip install -e . packaging requests tomlkit
run: uv pip install -e . packaging requests tomlkit prek
- 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
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
if [[ $(git status --porcelain) ]]; then
echo "dirty=true" >> $GITHUB_OUTPUT
fi
id: status
- name: Run pre-commit hooks to update other files
run: |
uv pip install pre-commit
pre-commit run --all || true
- uses: j178/prek-action@v2
if: steps.status.outputs.dirty == 'true'
- name: Create PR
run: |
Expand All @@ -83,4 +85,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 }}). <br> <br> *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'
if: steps.status.outputs.dirty == 'true' && (success() || failure())
4 changes: 1 addition & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ jobs:
- uses: actions/setup-python@v6.3.0
with:
python-version: '3.14'
- uses: pre-commit/action@v3.0.1
- run: pip install mypy numpy scipy vulture
- run: mypy
- run: pip install --upgrade vulture
- run: vulture

bandit:
Expand Down
32 changes: 26 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,31 @@ repos:
- id: ruff-format
files: ^mne/|^doc/|^tutorials/|^examples/|^tools/

# ty (type checking); deps installed by pre-commit(.ci), see [tool.ty] in
# pyproject.toml for the checked modules and silenced rules
- repo: https://github.com/astral-sh/ty-pre-commit
rev: v0.0.55
hooks:
- id: ty
additional_dependencies:
- antio # for mne/io/ant
- curryreader # for mne/io/curry
- edfio # for mne/io/edf
- eeglabio # for mne/io/eeglab
- h5py # for mne/io/snirf
- lazy_loader # for mne/io/__init__.py
- matplotlib
- mffpy # for mne/io/egi
- neo # for mne/io/neuralynx
- numpy
- pandas # for mne/io/base
- pymatreader # for mne/io/eeglab
- pymef # for mne/io/mef
- quantities # for mne/io/neuralynx
- scipy
- typing_extensions # TODO VERSION remove once we require Python 3.11
args: [--no-python-downloads, --no-project]

# Codespell
- repo: https://github.com/codespell-project/codespell
rev: v2.4.2
Expand Down Expand Up @@ -82,12 +107,7 @@ repos:
# We correctly use pull_request_trigger, and need Zizmor 2.0+ to configure the ignore
exclude: ^.github/workflows/automerge.yml

# The following are too slow to run on local commits, so let's only run on CIs:
#
# - repo: https://github.com/pre-commit/mirrors-mypy
# rev: v1.9.0
# hooks:
# - id: mypy
# The following is too slow to run on local commits, so let's only run on CIs:
#
# - repo: https://github.com/jendrikseipp/vulture
# rev: 'v2.11' # or any later Vulture version
Expand Down
8 changes: 0 additions & 8 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,12 @@ stages:
python -m pip install --progress-bar off --upgrade pip build
python -m pip install --only-binary=":all:" --progress-bar off -ve .[hdf5] --group=test
python -m pip uninstall -yq pytest-qt # don't want to set up display, etc. for this
pre-commit install --install-hooks
displayName: Install dependencies
- bash: make pre-commit
displayName: make pre-commit
condition: always()
- bash: make nesting
displayName: make nesting
condition: always()
- bash: make check-readme
displayName: make check-readme
condition: always()
- bash: mypy
displayName: mypy
condition: always()
- bash: vulture
displayName: vulture
condition: always()
Expand Down
1 change: 1 addition & 0 deletions doc/changes/dev/14036.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
More complete type checking has been implemented for :mod:`mne.io`, plus :class:`Epochs` and :class:`Evoked`, by `Eric Larson`_.
6 changes: 6 additions & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,11 +290,15 @@
"EpochsFIF": "mne.Epochs",
"EpochsEEGLAB": "mne.Epochs",
"EpochsKIT": "mne.Epochs",
"BaseRaw": "mne.io.Raw",
"RawANT": "mne.io.Raw",
"RawArtemis123": "mne.io.Raw",
"RawBCI2k": "mne.io.Raw",
"RawBDF": "mne.io.Raw",
"RawBOXY": "mne.io.Raw",
"RawBrainVision": "mne.io.Raw",
"RawBTi": "mne.io.Raw",
"RawCNT": "mne.io.Raw",
"RawCTF": "mne.io.Raw",
"RawCurry": "mne.io.Raw",
"RawEDF": "mne.io.Raw",
Expand All @@ -308,7 +312,9 @@
"RawKIT": "mne.io.Raw",
"RawNedf": "mne.io.Raw",
"RawNeuralynx": "mne.io.Raw",
"RawNicolet": "mne.io.Raw",
"RawNihon": "mne.io.Raw",
"RawNSX": "mne.io.Raw",
"RawMEF": "mne.io.Raw",
"RawNIRX": "mne.io.Raw",
"RawPersyst": "mne.io.Raw",
Expand Down
32 changes: 22 additions & 10 deletions mne/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from scipy.io import loadmat

from ._fiff.constants import FIFF
from ._fiff.meas_info import Info
from ._fiff.open import fiff_open
from ._fiff.tag import read_tag
from ._fiff.tree import dir_tree_find
Expand Down Expand Up @@ -67,7 +68,9 @@ class _AnnotationsExtrasDict(UserDict):
strings, integers, floats, or None.
"""

def __setitem__(self, key: str, value: str | int | float | None) -> None:
def __setitem__( # ty: ignore[invalid-method-override] # intentional narrowing
self, key: str, value: str | int | float | None
) -> None:
_validate_type(key, str, "key")
if key in ("onset", "duration", "description", "ch_names", "hed_string"):
raise ValueError(f"Key '{key}' is reserved and cannot be used in extras.")
Expand Down Expand Up @@ -112,7 +115,7 @@ def __init__(self, initlist=None):
initlist = [self._validate_value(v) for v in initlist]
super().__init__(initlist)

def __setitem__( # type: ignore[override]
def __setitem__( # ty: ignore[invalid-method-override] # intentional narrowing
self,
key: int | slice,
value,
Expand Down Expand Up @@ -624,7 +627,7 @@ def __iter__(self):

def __getitem__(self, key, *, with_ch_names=None, with_extras=True):
"""Propagate indexing and slicing to the underlying numpy structure."""
if isinstance(key, int_like):
if isinstance(key, int_like): # ty: ignore[invalid-argument-type] # __instancecheck__
out_keys = ("onset", "duration", "description", "orig_time")
out_vals = (
self.onset[key],
Expand All @@ -647,7 +650,7 @@ def __getitem__(self, key, *, with_ch_names=None, with_extras=True):
description=self.description[key],
orig_time=self.orig_time,
ch_names=self.ch_names[key],
extras=[self.extras[i] for i in np.arange(len(self.extras))[key]],
extras=[self.extras[i] for i in np.arange(len(self.extras))[key]], # ty: ignore[invalid-argument-type]
)

@fill_doc
Expand Down Expand Up @@ -722,7 +725,7 @@ def delete(self, idx):
self._duration = np.delete(self._duration, idx)
self._description = np.delete(self._description, idx)
self._ch_names = np.delete(self._ch_names, idx)
if isinstance(idx, int_like):
if isinstance(idx, int_like): # ty: ignore[invalid-argument-type] # __instancecheck__
del self.extras[idx]
elif len(idx) > 0:
# convert slice-like idx to ints, and delete list items in reverse order
Expand Down Expand Up @@ -1389,7 +1392,7 @@ def delete(self, idx):
indices.
"""
super().delete(idx)
if isinstance(idx, int_like):
if isinstance(idx, int_like): # ty: ignore[invalid-argument-type] # __instancecheck__
del self.hed_string._objs[idx]
del self.hed_string[idx]
else:
Expand Down Expand Up @@ -1487,6 +1490,14 @@ def to_data_frame(self, time_format="datetime"):
class EpochAnnotationsMixin:
"""Mixin class for Annotations in Epochs."""

# Attributes provided by the host class (BaseEpochs), declared here so the
# mixin methods type-check.
info: Info
events: np.ndarray
times: np.ndarray
_raw_sfreq: float
_annotations: Annotations | None

@property
def annotations(self): # noqa: D102
return self._annotations
Expand Down Expand Up @@ -1575,6 +1586,7 @@ def get_annotations_per_epoch(self, *, with_extras=False):
# check if annotations exist
if self.annotations is None:
return epoch_annot_list
assert self._annotations is not None

# when each epoch and annotation starts/stops
# no need to account for first_samp here...
Expand Down Expand Up @@ -1676,8 +1688,8 @@ def add_annotations_to_metadata(self, overwrite=False, *, with_extras=True):
return self

# get existing metadata DataFrame or instantiate an empty one
if self._metadata is not None:
metadata = self._metadata
if self._metadata is not None: # ty: ignore[unresolved-attribute] # host pandas attr
metadata = self._metadata # ty: ignore[unresolved-attribute] # host pandas attr
else:
data = np.empty((len(self.events), 0))
metadata = pd.DataFrame(data=data)
Expand Down Expand Up @@ -2266,9 +2278,9 @@ def _read_annotations_fif(fid, tree):
elif kind == FIFF.FIFF_MEAS_DATE:
orig_time = tag.data
try:
orig_time = float(orig_time) # old way
orig_time = float(tag.data) # old way
except TypeError:
orig_time = tuple(orig_time) # new way
orig_time = tuple(tag.data) # new way
elif kind == FIFF.FIFF_MNE_EPOCHS_DROP_LOG:
ch_names = tuple(tuple(x) for x in json.loads(tag.data))
elif kind == FIFF.FIFF_FREE_LIST:
Expand Down
3 changes: 2 additions & 1 deletion mne/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
# Copyright the MNE-Python contributors.

from copy import deepcopy
from typing import Any

DEFAULTS = dict(
DEFAULTS: dict[str, Any] = dict(
color=dict(
mag="darkblue",
grad="b",
Expand Down
Loading
Loading