diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml
index eb0a53865f7..2f70f77745e 100644
--- a/.github/workflows/autofix.yml
+++ b/.github/workflows/autofix.yml
@@ -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
@@ -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()
diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml
index 9210505c87c..bf9e817e53c 100644
--- a/.github/workflows/automerge.yml
+++ b/.github/workflows/automerge.yml
@@ -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"
diff --git a/.github/workflows/autopush.yml b/.github/workflows/autopush.yml
index cd0436a67f0..8675861d86e 100644
--- a/.github/workflows/autopush.yml
+++ b/.github/workflows/autopush.yml
@@ -3,7 +3,6 @@ on: # yamllint disable-line rule:truthy
push:
branches:
- dependabot/**
- - pre-commit-ci*
jobs:
autobot:
@@ -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
diff --git a/.github/workflows/spec_zero.yml b/.github/workflows/spec_zero.yml
index 08dec793a08..b8f3af63b50 100644
--- a/.github/workflows/spec_zero.yml
+++ b/.github/workflows/spec_zero.yml
@@ -47,7 +47,7 @@ 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
@@ -55,8 +55,13 @@ jobs:
- 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
@@ -64,10 +69,7 @@ jobs:
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: |
@@ -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 }}).
*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())
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 6a4d2578bf9..e0c20bfe38b 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -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:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 68213b504d5..5f664e13411 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -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
@@ -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
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index ce961495f39..4e43cb91949 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -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()
diff --git a/doc/changes/dev/14036.newfeature.rst b/doc/changes/dev/14036.newfeature.rst
new file mode 100644
index 00000000000..208d0aa1c24
--- /dev/null
+++ b/doc/changes/dev/14036.newfeature.rst
@@ -0,0 +1 @@
+More complete type checking has been implemented for :mod:`mne.io`, plus :class:`Epochs` and :class:`Evoked`, by `Eric Larson`_.
diff --git a/doc/conf.py b/doc/conf.py
index 358779583f0..66c2c36cbd9 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -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",
@@ -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",
diff --git a/mne/annotations.py b/mne/annotations.py
index 4ca44d8878b..e8ee5677e12 100644
--- a/mne/annotations.py
+++ b/mne/annotations.py
@@ -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
@@ -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.")
@@ -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,
@@ -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],
@@ -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
@@ -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
@@ -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:
@@ -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
@@ -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...
@@ -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)
@@ -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:
diff --git a/mne/defaults.py b/mne/defaults.py
index 41fc8603778..126a21cf135 100644
--- a/mne/defaults.py
+++ b/mne/defaults.py
@@ -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",
diff --git a/mne/epochs.py b/mne/epochs.py
index 442359f3d65..3f2919c9178 100644
--- a/mne/epochs.py
+++ b/mne/epochs.py
@@ -105,6 +105,7 @@
verbose,
warn,
)
+from .utils._typing import Self
from .utils.docs import fill_doc
from .viz import plot_drop_log, plot_epochs, plot_epochs_image, plot_topo_image_epochs
@@ -682,6 +683,7 @@ def __init__(
# requested
# we could do this with np.einsum, but iteration should be
# more memory safe in most instances
+ assert self._data is not None
for ii, epoch in enumerate(self._data):
self._data[ii] = np.dot(self._projector, epoch)
self.filename = filename if filename is not None else filename
@@ -1169,6 +1171,7 @@ def _compute_aggregate(self, picks, mode="mean"):
n_times = len(self.times)
if self.preload:
+ assert self._data is not None
n_events = len(self.events)
fun = _check_combine(mode, valid=("mean", "median", "std"))
data = fun(self._data)
@@ -1702,6 +1705,7 @@ def _get_data(
# in case there are no good events
if self.preload:
# we will store our result in our existing array
+ assert self._data is not None
data = self._data
else:
# we start out with an empty array, allocate only if necessary
@@ -1770,6 +1774,7 @@ def _get_data(
detrend_picks = self._detrend_picks
for idx, sel in enumerate(self.selection):
if self.preload: # from memory
+ assert self._data is not None
if self._do_delayed_proj:
epoch_noproj = self._data[idx]
epoch = self._project_epoch(epoch_noproj)
@@ -1793,6 +1798,7 @@ def _get_data(
good_idx.append(idx)
# store the epoch if there is a reason to (output or update)
+ assert epoch_out is not None
if out or self.preload:
# faster to pre-allocate, then trim as necessary
if n_out == 0 and not self.preload:
@@ -1989,6 +1995,7 @@ def apply_function(
The epochs object with transformed data.
"""
_check_preload(self, "epochs.apply_function")
+ assert self._data is not None
picks = _picks_to_idx(self.info, picks, exclude=(), with_ref_meg=False)
if not callable(fun):
@@ -2120,19 +2127,16 @@ def _repr_html_(self):
event_strings = None
t = _get_html_template("repr", "epochs.html.jinja")
+ fname = self.filename
t = t.render(
inst=self,
- filenames=(
- [Path(self.filename).name]
- if getattr(self, "filename", None) is not None
- else None
- ),
+ filenames=[Path(fname).name] if fname is not None else None,
event_counts=event_strings,
)
return t
@verbose
- def crop(self, tmin=None, tmax=None, include_tmax=True, verbose=None):
+ def crop(self, tmin=None, tmax=None, include_tmax=True, verbose=None) -> Self:
"""Crop a time interval from the epochs.
Parameters
@@ -2173,7 +2177,7 @@ def crop(self, tmin=None, tmax=None, include_tmax=True, verbose=None):
self.reject_tmax = self.tmax
return self
- def copy(self):
+ def copy(self) -> Self:
"""Return copy of Epochs instance.
Returns
@@ -3825,6 +3829,7 @@ def __init__(
):
raise ValueError("The events must only contain event numbers from event_id")
detrend_picks = self._detrend_picks
+ assert self._data is not None
for e in self._data:
# This is safe without assignment b/c there is no decim
self._detrend_offset_decim(e, detrend_picks)
@@ -4273,7 +4278,7 @@ def read_epochs(fname, proj=True, preload=True, verbose=None) -> "EpochsFIF":
Returns
-------
- epochs : instance of Epochs
+ epochs : instance of EpochsFIF
The epochs.
"""
return EpochsFIF(fname, proj, preload, verbose)
@@ -4478,6 +4483,7 @@ def _get_epoch_from_raw(self, idx, verbose=None):
"""Load one epoch from disk."""
# Find the right file and offset to use
event_samp = self.events[idx, 0]
+ assert self._raw is not None
for raw in self._raw:
idx = np.where(raw.event_samps == event_samp)[0]
if len(idx) == 1:
diff --git a/mne/evoked.py b/mne/evoked.py
index 36174f6bb1d..4f932a7f62e 100644
--- a/mne/evoked.py
+++ b/mne/evoked.py
@@ -65,6 +65,7 @@
verbose,
warn,
)
+from .utils._typing import Self
from .viz import (
plot_evoked,
plot_evoked_field,
@@ -474,13 +475,10 @@ def __repr__(self): # noqa: D105
@repr_html
def _repr_html_(self):
t = _get_html_template("repr", "evoked.html.jinja")
+ fname = self.filename
t = t.render(
inst=self,
- filenames=(
- [Path(self.filename).name]
- if getattr(self, "filename", None) is not None
- else None
- ),
+ filenames=[Path(fname).name] if fname is not None else None,
)
return t
@@ -991,7 +989,7 @@ def detrend(self, order=1, picks=None):
self.data[picks] = detrend(self.data[picks], order, axis=-1)
return self
- def copy(self):
+ def copy(self) -> Self:
"""Copy the instance of evoked.
Returns
@@ -1981,6 +1979,7 @@ def _read_evoked(fname, condition=None, kind="average", allow_maxshield=False):
if first_time is not None and nsamp is not None:
times = first_time + np.arange(nsamp) / info["sfreq"]
elif first is not None:
+ assert last is not None # always read together with first
nsamp = last - first + 1
times = np.arange(first, last + 1) / info["sfreq"]
else:
diff --git a/mne/io/_read_raw.py b/mne/io/_read_raw.py
index e4f2cfacb4a..2cc61e8142e 100644
--- a/mne/io/_read_raw.py
+++ b/mne/io/_read_raw.py
@@ -173,7 +173,7 @@ def read_raw(fname, *, preload=False, verbose=None, **kwargs) -> BaseRaw:
Returns
-------
- raw : mne.io.Raw
+ raw : instance of BaseRaw
Raw object.
"""
_, ext = split_name_ext(fname)
diff --git a/mne/io/ant/ant.py b/mne/io/ant/ant.py
index 9e314399c94..c3a2f1e94c1 100644
--- a/mne/io/ant/ant.py
+++ b/mne/io/ant/ant.py
@@ -221,13 +221,13 @@ def _parse_ch_types(
ch_names: list[str], eog: str | None, misc: str | None, ch_refs: list[str]
) -> list[str]:
"""Parse the channel types."""
- eog = re.compile(eog) if eog is not None else None
- misc = re.compile(misc) if misc is not None else None
+ eog_re = re.compile(eog) if eog is not None else None
+ misc_re = re.compile(misc) if misc is not None else None
ch_types = []
for ch in ch_names:
- if eog is not None and re.fullmatch(eog, ch):
+ if eog_re is not None and re.fullmatch(eog_re, ch):
ch_types.append("eog")
- elif misc is not None and re.fullmatch(misc, ch):
+ elif misc_re is not None and re.fullmatch(misc_re, ch):
ch_types.append("misc")
else:
ch_types.append("eeg")
diff --git a/mne/io/artemis123/artemis123.py b/mne/io/artemis123/artemis123.py
index 177b57c5db7..1a1c4003e92 100644
--- a/mne/io/artemis123/artemis123.py
+++ b/mne/io/artemis123/artemis123.py
@@ -5,6 +5,7 @@
import calendar
import datetime
import os.path as op
+from typing import Any
import numpy as np
from scipy.spatial.distance import cdist
@@ -43,7 +44,7 @@ def read_raw_artemis123(
Returns
-------
- raw : instance of Raw
+ raw : instance of RawArtemis123
A Raw object containing the data.
See Also
@@ -77,7 +78,7 @@ def _get_artemis123_info(fname, pos_fname=None):
"FLL_ResetLock",
]
- header_info = dict()
+ header_info: dict[str, Any] = dict()
header_info["filter_hist"] = []
header_info["comments"] = ""
header_info["channels"] = []
diff --git a/mne/io/base.py b/mne/io/base.py
index 597723d72b8..05086140ca1 100644
--- a/mne/io/base.py
+++ b/mne/io/base.py
@@ -11,6 +11,7 @@
from datetime import timedelta
from inspect import getfullargspec
from pathlib import Path
+from typing import Any
import numpy as np
@@ -18,6 +19,7 @@
from .._fiff.constants import FIFF
from .._fiff.meas_info import (
ContainsMixin,
+ Info,
SetChannelsMixin,
_ensure_infos_match,
_unit2human,
@@ -183,6 +185,7 @@ class BaseRaw(
# consider adding it to the Attributes list for Raw in mne/io/fiff/raw.py.
_extra_attributes = ()
+ _filenames: list[Path | None]
@verbose
def __init__(
@@ -281,7 +284,12 @@ def __init__(
# STI 014 channel is native only to fif ... for all other formats
# this was artificially added by the IO procedure, so remove it
ch_names = list(info["ch_names"])
- if "STI 014" in ch_names and self.filenames[0].suffix != ".fif":
+ first_fname = self.filenames[0]
+ if (
+ "STI 014" in ch_names
+ and first_fname is not None
+ and first_fname.suffix != ".fif"
+ ):
ch_names.remove("STI 014")
# Each channel in the data must have a corresponding channel in
@@ -347,6 +355,7 @@ def apply_gradient_compensation(self, grade, verbose=None):
# We might need to apply it to our data now
if self.preload:
logger.info("Applying compensator to loaded data")
+ assert self._data is not None
lims = np.concatenate(
[np.arange(0, len(self.times), 10000), [len(self.times)]]
)
@@ -616,7 +625,7 @@ def _first_time(self):
return self.first_samp / float(self.info["sfreq"])
@property
- def first_samp(self):
+ def first_samp(self) -> int:
"""The first data sample.
See :term:`first_samp`.
@@ -629,7 +638,7 @@ def first_time(self):
return self._first_time
@property
- def last_samp(self):
+ def last_samp(self) -> int:
"""The last data sample."""
return self.first_samp + sum(self._raw_lengths) - 1
@@ -698,17 +707,19 @@ def filenames(self) -> tuple[Path | None, ...]:
def filenames(self, value):
"""The filenames used, cast to list of paths.""" # noqa: D401
_validate_type(value, (list, tuple), "filenames")
- if isinstance(value, tuple):
- value = list(value)
- for k, elt in enumerate(value):
- if elt is not None:
- value[k] = _check_fname(elt, overwrite="read", must_exist=False)
- if not value[k].exists():
- # check existence separately from _check_fname since some
- # fileformats use directories instead of files and '_check_fname'
- # does not handle it correctly.
- raise FileNotFoundError(f"File {value[k]} not found.")
- self._filenames = list(value)
+ filenames: list[Path | None] = []
+ for elt in value:
+ if elt is None:
+ filenames.append(None)
+ continue
+ fname = _check_fname(elt, overwrite="read", must_exist=False)
+ if not fname.exists():
+ # check existence separately from _check_fname since some
+ # fileformats use directories instead of files and '_check_fname'
+ # does not handle it correctly.
+ raise FileNotFoundError(f"File {fname} not found.")
+ filenames.append(fname)
+ self._filenames = filenames
@verbose
def set_annotations(
@@ -772,13 +783,13 @@ def set_annotations(
def __del__(self): # noqa: D105
# remove file for memmap
- if hasattr(self, "_data") and getattr(self._data, "filename", None) is not None:
+ fname = getattr(getattr(self, "_data", None), "filename", None)
+ if fname is not None:
# First, close the file out; happens automatically on del
- filename = self._data.filename
del self._data
# Now file can be removed
try:
- os.remove(filename)
+ os.remove(fname)
except OSError:
pass # ignore file that no longer exists
@@ -876,6 +887,7 @@ def __getitem__(self, item):
def _getitem(self, item, return_times=True):
sel, start, stop = self._parse_get_set_params(item)
if self.preload:
+ assert self._data is not None
data = self._data[sel, start:stop]
else:
data = self._read_segment(start=start, stop=stop, sel=sel)
@@ -893,6 +905,7 @@ def _getitem(self, item, return_times=True):
def __setitem__(self, item, value):
"""Set raw data content."""
_check_preload(self, "Modifying data of Raw")
+ assert self._data is not None
sel, start, stop = self._parse_get_set_params(item)
# set the data
self._data[sel, start:stop] = value
@@ -1099,6 +1112,7 @@ def apply_function(
The raw object with transformed data.
"""
_check_preload(self, "raw.apply_function")
+ assert self._data is not None
picks = _picks_to_idx(self.info, picks, exclude=(), with_ref_meg=False)
if not callable(fun):
@@ -1281,6 +1295,7 @@ def notch_filter(
fs = float(self.info["sfreq"])
picks = _picks_to_idx(self.info, picks, exclude=(), none="data_or_ica")
_check_preload(self, "raw.notch_filter")
+ assert self._data is not None
onsets, ends = _annotations_starts_stops(self, skip_by_annotation, invert=True)
logger.info(
"Filtering raw data in %d contiguous segment%s", len(onsets), _pl(onsets)
@@ -1439,10 +1454,12 @@ def resample(
ratio, n_news = ratio[0], np.array(n_news, int)
new_offsets = np.cumsum([0] + list(n_news))
if self.preload:
+ assert self._data is not None
new_data = np.empty((len(self.ch_names), new_offsets[-1]), self._data.dtype)
for ri, (n_orig, n_new) in enumerate(zip(self._raw_lengths, n_news)):
this_sl = slice(new_offsets[ri], new_offsets[ri + 1])
if self.preload:
+ assert self._data is not None
data_chunk = self._data[:, offsets[ri] : offsets[ri + 1]]
new_data[:, this_sl] = resample(data_chunk, **kwargs)
# In empirical testing, it was faster to resample all channels
@@ -1689,6 +1706,7 @@ def crop(
self.filenames = [self.filenames[ri] for ri in keepers]
if self.preload:
# slice and copy to avoid the reference to large array
+ assert self._data is not None
self._data = self._data[:, smin : smax + 1].copy()
annotations = self.annotations
@@ -1858,6 +1876,7 @@ def save(
)
if self.preload:
+ assert self._data is not None
if np.iscomplexobj(self._data):
warn(
"Saving raw file with complex data. Loading with command-line MNE "
@@ -2179,6 +2198,7 @@ def append(self, raws, preload=None):
else:
this_data = self._data
+ assert this_data is not None
# allocate the buffer
_data = _allocate_data(preload, (nchan, nsamp), this_data.dtype)
_data[:, 0 : c_ns[0]] = this_data
@@ -2325,6 +2345,7 @@ def add_events(self, events, stim_channel=None, replace=False):
)
if not all(idx == events[:, 0]):
raise ValueError("event sample numbers must be integers")
+ assert self._data is not None
if replace:
self._data[pick, :] = 0.0
self._data[pick, idx - self.first_samp] += events[:, 2]
@@ -2783,6 +2804,11 @@ def filenames(self) -> tuple[Path, ...]:
class _RawShell:
"""Create a temporary raw object."""
+ # attributes populated externally (e.g. by the FIF reader)
+ info: Info
+ orig_format: str | None
+ _raw_extras: dict[str, Any]
+
def __init__(self):
self.first_samp = None
self.last_samp = None
@@ -2793,6 +2819,8 @@ def __init__(self):
@property
def n_times(self): # noqa: D102
+ assert self.first_samp is not None
+ assert self.last_samp is not None
return self.last_samp - self.first_samp + 1
@property
diff --git a/mne/io/besa/besa.py b/mne/io/besa/besa.py
index d6d4ee9657c..2d3cb75cb86 100644
--- a/mne/io/besa/besa.py
+++ b/mne/io/besa/besa.py
@@ -61,6 +61,7 @@ def _read_evoked_besa_avr(fname, verbose):
# Consolidate channel names
if new_style:
+ assert ch_names is not None
if len(ch_names) != len(data):
raise RuntimeError(
"Mismatch between the number of channel names defined in "
@@ -85,6 +86,7 @@ def _read_evoked_besa_avr(fname, verbose):
ch_names = [f"CH{i + 1:02d}" for i in range(len(data))]
# Consolidate channel types
+ assert ch_names is not None
if ch_types is None:
logger.info("Marking all channels as EEG.")
ch_types = ["eeg"] * len(ch_names)
@@ -96,14 +98,14 @@ def _read_evoked_besa_avr(fname, verbose):
if "Npts" in fields:
fields["Npts"] = int(fields["Npts"])
if fields["Npts"] != data.shape[1]:
- logger.warn(
+ logger.warning(
f"The size of the data matrix ({data.shape}) does not "
f'match the "Npts" field ({fields["Npts"]}).'
)
if "Nchan" in fields:
fields["Nchan"] = int(fields["Nchan"])
if fields["Nchan"] != data.shape[0]:
- logger.warn(
+ logger.warning(
f"The size of the data matrix ({data.shape}) does not "
f'match the "Nchan" field ({fields["Nchan"]}).'
)
@@ -166,14 +168,14 @@ def _read_evoked_besa_mul(fname, verbose):
if "TimePoints" in fields:
fields["TimePoints"] = int(fields["TimePoints"])
if fields["TimePoints"] != data.shape[0]:
- logger.warn(
+ logger.warning(
f"The size of the data matrix ({data.shape}) does not "
f'match the "TimePoints" field ({fields["TimePoints"]}).'
)
if "Channels" in fields:
fields["Channels"] = int(fields["Channels"])
if fields["Channels"] != data.shape[1]:
- logger.warn(
+ logger.warning(
f"The size of the data matrix ({data.shape}) does not "
f'match the "Channels" field ({fields["Channels"]}).'
)
diff --git a/mne/io/boxy/boxy.py b/mne/io/boxy/boxy.py
index 9a131730a51..26e301a61b8 100644
--- a/mne/io/boxy/boxy.py
+++ b/mne/io/boxy/boxy.py
@@ -3,6 +3,7 @@
# Copyright the MNE-Python contributors.
import re as re
+from typing import Any
import numpy as np
@@ -63,7 +64,7 @@ def __init__(self, fname, preload=False, verbose=None):
# Read header file and grab some info.
start_line = np.inf
col_names = mrk_col = filetype = mrk_data = end_line = None
- raw_extras = dict()
+ raw_extras: dict[str, Any] = dict()
raw_extras["offsets"] = list() # keep track of our offsets
sfreq = None
fname = str(_check_fname(fname, "read", True, "fname"))
@@ -80,6 +81,7 @@ def __init__(self, fname, preload=False, verbose=None):
end_line = line_num
break
if mrk_col is not None:
+ assert mrk_data is not None
if filetype == "non-parsed":
# Non-parsed files have different lines lengths.
crnt_line = i_line.rsplit(" ")[0]
@@ -164,6 +166,7 @@ def __init__(self, fname, preload=False, verbose=None):
ch["cal"] = cal
# Determine how long our data is.
+ assert end_line is not None
delta = end_line - start_line
assert len(raw_extras["offsets"]) == delta + 1
if filetype == "non-parsed":
diff --git a/mne/io/brainvision/brainvision.py b/mne/io/brainvision/brainvision.py
index 96c5cfa456b..de06e7aeaab 100644
--- a/mne/io/brainvision/brainvision.py
+++ b/mne/io/brainvision/brainvision.py
@@ -286,11 +286,12 @@ def _read_mrk(fname):
# LookupError exception; Python recognize ANSI decoding as cp1252
if codepage == "ANSI":
codepage = "cp1252"
- txt = txt.decode(codepage)
+ decoded = txt.decode(codepage)
except UnicodeDecodeError:
# if UTF-8 (new standard) or explicit codepage setting fails, fallback to
# Latin-1, which is Windows default and implicit standard in older recordings
- txt = txt.decode("latin-1")
+ decoded = txt.decode("latin-1")
+ txt = decoded
# extract Marker Infos block
onset, duration, type_, description = [], [], [], []
@@ -565,12 +566,13 @@ def _aux_hdr_info(hdr_fname, sfreq_override=None):
# LookupError exception; Python recognize ANSI decoding as cp1252
if codepage == "ANSI":
codepage = "cp1252"
- settings = settings.decode(codepage)
+ decoded = settings.decode(codepage)
except UnicodeDecodeError:
# if UTF-8 (new standard) or explicit codepage setting fails, fallback to
# Latin-1, which is Windows default and implicit standard in older
# recordings
- settings = settings.decode("latin-1")
+ decoded = settings.decode("latin-1")
+ settings = decoded
if settings.find("[Comment]") != -1:
params, settings = settings.split("[Comment]")
@@ -744,6 +746,7 @@ def _get_hdr_info(hdr_fname, eog, misc, scale, overrides=None):
with open(data_fname, "rb") as fid:
fid.seek(0, 2)
n_bytes = fid.tell()
+ assert isinstance(fmt, str)
n_samples = n_bytes // _fmt_byte_dict[fmt] // nchan
ch_names = [""] * nchan
@@ -936,6 +939,7 @@ def _get_hdr_info(hdr_fname, eog, misc, scale, overrides=None):
if ch in synthesized_chs:
continue
# double check alignment with channel by using the hw settings
+ assert idx_amp is not None
if idx == idx_amp:
line_amp = settings[idx + i]
else:
@@ -1227,7 +1231,7 @@ def read_raw_brainvision(
class _BVEventParser(_DefaultEventParser):
"""Parse standard brainvision events, accounting for non-standard ones."""
- def __call__(self, description):
+ def __call__(self, description): # ty: ignore[invalid-method-override] # intentional
"""Parse BrainVision event codes (like `Stimulus/S 11`) to ints."""
offsets = _BV_EVENT_IO_OFFSETS
diff --git a/mne/io/bti/bti.py b/mne/io/bti/bti.py
index 17e27c7e37d..d74cdd19fb5 100644
--- a/mne/io/bti/bti.py
+++ b/mne/io/bti/bti.py
@@ -6,6 +6,7 @@
import os.path as op
from io import BytesIO
from itertools import count
+from typing import Any
import numpy as np
@@ -548,7 +549,7 @@ def _read_config(fname):
cfg["chs"] += [ch]
_correct_offset(fid) # before and after
- dta = dict()
+ dta: dict[str, Any] = dict()
if ch["ch_type"] in [BTI.CHTYPE_MEG, BTI.CHTYPE_REFERENCE]:
dev = {
"device_info": read_dev_header(fid),
diff --git a/mne/io/cnt/cnt.py b/mne/io/cnt/cnt.py
index eaf54b0dabc..5bc2506c7d3 100644
--- a/mne/io/cnt/cnt.py
+++ b/mne/io/cnt/cnt.py
@@ -249,7 +249,7 @@ def read_raw_cnt(
Returns
-------
- raw : instance of RawCNT.
+ raw : instance of RawCNT
The raw data.
See :class:`mne.io.Raw` for documentation of attributes and methods.
@@ -634,6 +634,7 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult):
)
block[:f_channels, block_slice] = row
if "stim_channel" in self._raw_extras[fi]:
+ assert stim_ch is not None
_data_start = start + sample_start
_data_stop = start + sample_stop
block[-1] = stim_ch[_data_start:_data_stop]
diff --git a/mne/io/ctf/eeg.py b/mne/io/ctf/eeg.py
index cd39bc980e0..b6d098d48cd 100644
--- a/mne/io/ctf/eeg.py
+++ b/mne/io/ctf/eeg.py
@@ -6,6 +6,7 @@
from os import listdir
from os.path import join
+from typing import Any
import numpy as np
@@ -30,7 +31,7 @@ def _read_eeg(directory):
if not found:
logger.info(" Separate EEG position data file not present.")
return
- eeg = dict(
+ eeg: dict[str, Any] = dict(
labels=list(),
kinds=list(),
ids=list(),
diff --git a/mne/io/ctf/hc.py b/mne/io/ctf/hc.py
index 22acceda0ca..e537710af4a 100644
--- a/mne/io/ctf/hc.py
+++ b/mne/io/ctf/hc.py
@@ -27,8 +27,8 @@
def _read_one_coil_point(fid):
"""Read coil coordinate information from the hc file."""
# Descriptor
- one = "#"
- while len(one) > 0 and one[0] == "#":
+ one = b"#"
+ while len(one) > 0 and one[0:1] == b"#":
one = fid.readline()
if len(one) == 0:
return None
diff --git a/mne/io/ctf/info.py b/mne/io/ctf/info.py
index 685a20792d3..6b6eadd6a63 100644
--- a/mne/io/ctf/info.py
+++ b/mne/io/ctf/info.py
@@ -7,6 +7,7 @@
import os.path as op
from calendar import timegm
from time import strptime
+from typing import Any
import numpy as np
@@ -182,7 +183,7 @@ def _convert_channel_info(res4, t, use_eeg_pos):
this_comp = None
for k, cch in enumerate(res4["chs"]):
cal = float(1.0 / (cch["proper_gain"] * cch["qgain"]))
- ch = dict(
+ ch: dict[str, Any] = dict(
scanno=k + 1,
range=1.0,
cal=cal,
@@ -398,7 +399,7 @@ def _convert_comp_data(res4):
def _pick_eeg_pos(c):
"""Pick EEG positions."""
- eeg = dict(
+ eeg: dict[str, Any] = dict(
coord_frame=FIFF.FIFFV_COORD_HEAD,
assign_to_chs=False,
labels=list(),
diff --git a/mne/io/ctf/res4.py b/mne/io/ctf/res4.py
index b2ecb9dc304..4d246fed1d3 100644
--- a/mne/io/ctf/res4.py
+++ b/mne/io/ctf/res4.py
@@ -69,6 +69,12 @@ def _read_filter(fid):
return f
+def _to_dict(arr: np.ndarray) -> dict[str, np.ndarray]:
+ # Convert a structured array to a dict of arrays
+ assert arr.dtype.names is not None
+ return {name: arr[name] for name in arr.dtype.names}
+
+
def _read_comp_coeff(fid, d):
"""Read compensation coefficients."""
# Read the coefficients and initialize
@@ -85,7 +91,7 @@ def _read_comp_coeff(fid, d):
("coeffs", ">f8", CTF.CTFV_MAX_BALANCING),
]
)
- comps = np.fromfile(fid, dt, d["ncomp"])
+ comps = _to_dict(np.fromfile(fid, dt, d["ncomp"]))
for k in range(d["ncomp"]):
comp = dict()
d["comp"].append(comp)
@@ -201,16 +207,14 @@ def _read_res4(dsdir):
("head_coil", _coil_dt, CTF.CTFV_MAX_COILS),
]
)
- chs = np.fromfile(fid, _ch_dt, res["nchan"])
+ chs = _to_dict(np.fromfile(fid, _ch_dt, res["nchan"]))
for coil in (chs["coil"], chs["head_coil"]):
coil["pos"] /= 100.0
coil["area"] *= 1e-4
# convert to dict
- chs = [dict(zip(chs.dtype.names, x)) for x in chs]
- for ch in chs:
- for key, val in ch.items():
- ch[key] = _auto_cast(val)
- res["chs"] = chs
+ res["chs"] = [
+ {key: _auto_cast(chs[key][ci]) for key in chs} for ci in range(res["nchan"])
+ ]
for k in range(res["nchan"]):
res["chs"][k]["ch_name"] = res["ch_names"][k]
diff --git a/mne/io/curry/curry.py b/mne/io/curry/curry.py
index c1ffcad96fb..4d023a4ad69 100644
--- a/mne/io/curry/curry.py
+++ b/mne/io/curry/curry.py
@@ -42,6 +42,13 @@
CURRY_SUFFIX_LABELS = [".cdt.dpa", ".cdt.dpo", ".rs3"]
+def _search(pattern, text):
+ """Search ``text`` for ``pattern`` and return the (guaranteed) match."""
+ match = re.compile(pattern).search(text)
+ assert match is not None
+ return match
+
+
def _get_curry_version(fname):
"""Check out the curry file version."""
fname_hdr = _check_curry_header_filename(_check_curry_filename(fname))
@@ -50,16 +57,10 @@ def _get_curry_version(fname):
"Curry 7"
if ".dap" in str(fname_hdr)
else "Curry 8"
- if re.compile(r"FileVersion\s*=\s*[0-9]+")
- .search(content_hdr)
- .group(0)
- .split()[-1][0]
+ if _search(r"FileVersion\s*=\s*[0-9]+", content_hdr).group(0).split()[-1][0]
== "8"
else "Curry 9"
- if re.compile(r"FileVersion\s*=\s*[0-9]+")
- .search(content_hdr)
- .group(0)
- .split()[-1][0]
+ if _search(r"FileVersion\s*=\s*[0-9]+", content_hdr).group(0).split()[-1][0]
== "9"
else None
)
@@ -123,12 +124,8 @@ def _check_curry_labels_filename(fname):
def _check_curry_sfreq_consistency(fname_hdr):
content_hdr = fname_hdr.read_text()
- stime = float(
- re.compile(r"SampleTimeUsec\s*=\s*.+").search(content_hdr).group(0).split()[-1]
- )
- sfreq = float(
- re.compile(r"SampleFreqHz\s*=\s*.+").search(content_hdr).group(0).split()[-1]
- )
+ stime = float(_search(r"SampleTimeUsec\s*=\s*.+", content_hdr).group(0).split()[-1])
+ sfreq = float(_search(r"SampleFreqHz\s*=\s*.+", content_hdr).group(0).split()[-1])
if stime == 0:
raise ValueError("Header file indicates a sampling interval of 0µs.")
if not np.isclose(1e6 / stime, sfreq):
@@ -150,7 +147,7 @@ def _get_curry_meas_info(fname):
# read meas_date
meas_date = [
- int(re.compile(rf"{v}\s*=\s*-?\d+").search(content_hdr).group(0).split()[-1])
+ int(_search(rf"{v}\s*=\s*-?\d+", content_hdr).group(0).split()[-1])
for v in [
"StartYear",
"StartMonth",
@@ -162,21 +159,15 @@ def _get_curry_meas_info(fname):
]
]
try:
+ year, month, day, hour, minute, second, millisec = meas_date
meas_date = datetime(
- *meas_date[:-1],
- meas_date[-1] * 1000, # -> microseconds
- timezone.utc,
+ year, month, day, hour, minute, second, millisec * 1000, timezone.utc
)
except Exception:
meas_date = None
# read datatype
- byteorder = (
- re.compile(r"DataByteOrder\s*=\s*[A-Z]+")
- .search(content_hdr)
- .group()
- .split()[-1]
- )
+ byteorder = _search(r"DataByteOrder\s*=\s*[A-Z]+", content_hdr).group().split()[-1]
is_ascii = byteorder == "ASCII"
# amplifier info
@@ -186,8 +177,7 @@ def _get_curry_meas_info(fname):
# TODO - FUTURE ENHANCEMENT
# # there can be filter details in AmplifierInfo, too
amp_info = (
- re.compile(r"AmplifierInfo\s*=.*\n")
- .search(content_hdr)
+ _search(r"AmplifierInfo\s*=.*\n", content_hdr)
.group()
.strip("\n")
.split("= ")[-1]
@@ -573,6 +563,7 @@ def _set_chanloc_curry(
# transform mode
pos = ch_loc[:3] # just the inner coil for MEG
pos = apply_trans(curry_dev_dev_t, pos)
+ assert ch_normals_meg is not None
nn = ch_normals_meg[i]
assert np.isclose(np.linalg.norm(nn), 1.0, atol=1e-4)
nn /= np.linalg.norm(nn)
@@ -748,7 +739,7 @@ def read_raw_curry(
inst = Epochs(inst, **curry_epoch_info)
if rectype == "evoked":
raise NotImplementedError # not sure this is even supported format
- return inst
+ return inst # ty: ignore[invalid-return-type] # epochs branch
class RawCurry(BaseRaw):
@@ -863,9 +854,9 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult):
if self._raw_extras[fi]["is_ascii"]:
if isinstance(idx, slice):
idx = np.arange(idx.start, idx.stop)
- block = np.loadtxt(
- self.filenames[0], skiprows=start, max_rows=stop - start, ndmin=2
- ).T
+ fname = self.filenames[0]
+ assert fname is not None
+ block = np.loadtxt(fname, skiprows=start, max_rows=stop - start, ndmin=2).T
_mult_cal_one(data, block, idx, cals, mult)
else:
diff --git a/mne/io/edf/edf.py b/mne/io/edf/edf.py
index 5421d7a510a..960f90d2134 100644
--- a/mne/io/edf/edf.py
+++ b/mne/io/edf/edf.py
@@ -9,6 +9,7 @@
from datetime import date, datetime, timedelta, timezone
from enum import Enum
from pathlib import Path
+from typing import Any
import numpy as np
from scipy.interpolate import interp1d
@@ -591,6 +592,7 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult):
def _read_ch(fid, subtype, samp, dtype_byte, dtype=None):
"""Read a number of samples for a single channel."""
+ assert dtype is not None
# BDF
if subtype == "bdf":
ch_data = read_from_file_or_buffer(fid, dtype=dtype, count=samp * dtype_byte)
@@ -1074,7 +1076,7 @@ def _read_edf_header(
exclude_after_unique=False,
):
"""Read header information from EDF+ or BDF file."""
- edf_info = {"events": []}
+ edf_info: dict[str, Any] = {"events": []}
with _gdf_edf_get_fid(fname) as fid:
fid.read(8) # version (unused here)
@@ -1350,7 +1352,7 @@ def _check_dtype_byte(types):
def _read_gdf_header(fname, exclude, include=None):
"""Read GDF 1.x and GDF 2.x header info."""
- edf_info = dict()
+ edf_info: dict[str, Any] = dict()
events = None
with _gdf_edf_get_fid(fname) as fid:
@@ -2090,7 +2092,7 @@ def read_raw_bdf(
Returns
-------
- raw : instance of RawEDF
+ raw : instance of RawBDF
The raw instance.
See :class:`mne.io.Raw` for documentation of attributes and methods.
@@ -2277,7 +2279,9 @@ def _read_annotations_edf(annotations, ch_names=None, encoding="utf8"):
else:
this_chan = chan.astype(np.int64)
# Exploit np vectorized processing
- tals.extend(np.uint8([this_chan % 256, this_chan // 256]).flatten("F"))
+ tals.extend(
+ np.array([this_chan % 256, this_chan // 256], np.uint8).flatten("F")
+ )
try:
triggers = re.findall(pat, tals.decode(encoding))
except UnicodeDecodeError as e:
@@ -2286,7 +2290,7 @@ def _read_annotations_edf(annotations, ch_names=None, encoding="utf8"):
" You might want to try setting \"encoding='latin1'\"."
) from e
- events = {}
+ events: dict[str, Any] = {}
offset = 0.0
for k, ev in enumerate(triggers):
onset = float(ev[0]) + offset
diff --git a/mne/io/eeglab/eeglab.py b/mne/io/eeglab/eeglab.py
index 6e651c639fd..c9f3fb89e67 100644
--- a/mne/io/eeglab/eeglab.py
+++ b/mne/io/eeglab/eeglab.py
@@ -385,7 +385,7 @@ def read_epochs_eeglab(
Returns
-------
- EpochsEEGLAB : instance of BaseEpochs
+ EpochsEEGLAB : instance of EpochsEEGLAB
The epochs.
See Also
@@ -697,6 +697,7 @@ def __init__(
# now fill up the event array
events = np.zeros((eeg.trials, 3), dtype=int)
+ assert event_id is not None
for idx in range(0, eeg.trials):
if idx == 0:
prev_stim = 0
@@ -711,6 +712,8 @@ def __init__(
logger.info(f"Extracting parameters from {input_fname}...")
info, eeg_montage, _ = _get_info(eeg, eog=eog, montage_units=montage_units)
+ assert event_id is not None
+ assert events is not None
for key, val in event_id.items():
if val not in events[:, 2]:
raise ValueError(f"No matching events found for {key} (event id {val})")
diff --git a/mne/io/egi/egimff.py b/mne/io/egi/egimff.py
index c603f3c5a7c..0efa9839704 100644
--- a/mne/io/egi/egimff.py
+++ b/mne/io/egi/egimff.py
@@ -10,6 +10,7 @@
import re
from collections import OrderedDict
from pathlib import Path
+from typing import Any
import numpy as np
@@ -536,7 +537,9 @@ def __init__(
first_samps = [0]
last_samps = [egi_info["last_samps"][-1] - 1]
- annot = dict(onset=list(), duration=list(), description=list(), extras=list())
+ annot: dict[str, Any] = dict(
+ onset=list(), duration=list(), description=list(), extras=list()
+ )
if len(idx["pns"]):
# PNS Data is present and should be read:
@@ -676,6 +679,7 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult):
this_block_info = _block_r(fid)
if this_block_info is not None:
current_block_info = this_block_info
+ assert current_block_info is not None
fid.seek(current_block_info["block_size"], 1)
current_block += 1
@@ -691,6 +695,7 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult):
if this_block_info is not None:
current_block_info = this_block_info
+ assert current_block_info is not None
to_read = current_block_info["nsamples"] * current_block_info["nc"]
block_data = np.fromfile(fid, dtype, to_read)
block_data = block_data.reshape(n_channels, -1, order="C")
diff --git a/mne/io/egi/general.py b/mne/io/egi/general.py
index ff196c0038f..da4471e283b 100644
--- a/mne/io/egi/general.py
+++ b/mne/io/egi/general.py
@@ -95,7 +95,9 @@ def _get_signalfname(filepath):
all_files = {}
infofiles = list()
for binfile in binfiles:
- bin_num_str = re.search(r"\d+", binfile).group()
+ match = re.search(r"\d+", binfile)
+ assert match is not None
+ bin_num_str = match.group()
infofile = "info" + bin_num_str + ".xml"
infofiles.append(infofile)
info_obj = XML.from_file(os.path.join(filepath, infofile))
@@ -122,7 +124,7 @@ def _block_r(fid):
header_size = np.fromfile(fid, dtype=np.dtype("i4"), count=1).item()
block_size = np.fromfile(fid, dtype=np.dtype("i4"), count=1).item()
hl = int(block_size / 4)
- nc = np.fromfile(fid, dtype=np.dtype("i4"), count=1).item()
+ nc = int(np.fromfile(fid, dtype=np.dtype("i4"), count=1).item())
nsamples = int(hl / nc)
np.fromfile(fid, dtype=np.dtype("i4"), count=nc) # sigoffset
sigfreq = np.fromfile(fid, dtype=np.dtype("i4"), count=nc)
diff --git a/mne/io/eyelink/_utils.py b/mne/io/eyelink/_utils.py
index e66b1855886..6aa6d6441c8 100644
--- a/mne/io/eyelink/_utils.py
+++ b/mne/io/eyelink/_utils.py
@@ -6,6 +6,7 @@
import re
from datetime import datetime, timedelta, timezone
+from typing import Any
import numpy as np
@@ -14,7 +15,8 @@
from ...annotations import Annotations
from ...utils import _check_pandas_installed, logger, warn
-EYELINK_COLS = {
+# heterogeneous nested structure (tuples of column names or sub-dicts by eye)
+EYELINK_COLS: dict[str, Any] = {
"timestamp": ("time",),
"pos": {
"left": ("xpos_left", "ypos_left", "pupil_left"),
@@ -129,7 +131,7 @@ def _parse_recording_blocks(fname):
if line.startswith("START"): # start of recording block
is_recording_block = True
# Initialize container for new block data
- current_block = {
+ current_block: dict[str, Any] = {
"samples": [],
"events": {
"START": [],
@@ -1030,7 +1032,9 @@ def _parse_calibration(
avg_error = float(line.split("avg.")[0].split()[-1]) # e.g. 0.3
max_error = float(line.split("max")[0].split()[-1]) # e.g. 0.9
- n_points = int(regex.search(model).group()) # e.g. 13
+ match = regex.search(model)
+ assert match is not None
+ n_points = int(match.group()) # e.g. 13
n_points *= 2 if "LR" in line else 1 # one point per eye if "LR"
# The next n_point lines contain the validation data
diff --git a/mne/io/fieldtrip/utils.py b/mne/io/fieldtrip/utils.py
index f855b7cf0cf..b87a9470aa6 100644
--- a/mne/io/fieldtrip/utils.py
+++ b/mne/io/fieldtrip/utils.py
@@ -202,7 +202,7 @@ def _set_sfreq(ft_struct):
"FieldTrip structure contained multiple sample rates, trying the "
f"first of:\n{sfreq} Hz"
)
- sfreq = float(sfreq.ravel()[0])
+ sfreq = float(np.asarray(sfreq).ravel()[0])
return sfreq
diff --git a/mne/io/fiff/raw.py b/mne/io/fiff/raw.py
index aad07690de8..480a312a210 100644
--- a/mne/io/fiff/raw.py
+++ b/mne/io/fiff/raw.py
@@ -5,6 +5,7 @@
import copy
import os.path as op
from pathlib import Path
+from typing import Any
import numpy as np
@@ -335,7 +336,9 @@ def _read_raw_file(
# reformat raw_extras to be a dict of list/ndarray rather than
# list of dict (faster access)
- raw_extras = {key: [r[key] for r in raw_extras] for key in raw_extras[0]}
+ raw_extras: dict[str, Any] = {
+ key: [r[key] for r in raw_extras] for key in raw_extras[0]
+ }
for key in raw_extras:
if key != "ent": # dict or None
raw_extras[key] = np.array(raw_extras[key], int)
diff --git a/mne/io/hitachi/hitachi.py b/mne/io/hitachi/hitachi.py
index 6be15ce79ac..efadfd92754 100644
--- a/mne/io/hitachi/hitachi.py
+++ b/mne/io/hitachi/hitachi.py
@@ -233,7 +233,9 @@ def _get_hitachi_info(fname, S_offset, D_offset, ignore_names):
elif kind == "Wave Length":
ch_regex = re.compile(r"^(.*)\(([0-9\.]+)\)$")
for ent in parts:
- _, v = ch_regex.match(ent).groups()
+ m = ch_regex.match(ent)
+ assert m is not None
+ _, v = m.groups()
ch_wavelengths[ent] = float(v)
elif kind == "Data":
break
@@ -268,6 +270,7 @@ def _get_hitachi_info(fname, S_offset, D_offset, ignore_names):
"3x11": "ETG-4000",
}
_check_option("Hitachi mode", mode, sorted(names))
+ assert mode is not None
n_row, n_col = (int(x) for x in mode.split("x"))
logger.info(f"Constructing pairing matrix for {names[mode]} ({mode})")
pairs = _compute_pairs(n_row, n_col, n=1 + (mode == "3x3"))
diff --git a/mne/io/kit/kit.py b/mne/io/kit/kit.py
index 53006dba43d..4a5f94caf2f 100644
--- a/mne/io/kit/kit.py
+++ b/mne/io/kit/kit.py
@@ -1021,7 +1021,7 @@ def read_epochs_kit(
Returns
-------
- EpochsKIT : instance of BaseEpochs
+ EpochsKIT : instance of EpochsKIT
The epochs.
See Also
diff --git a/mne/io/mef/mef.py b/mne/io/mef/mef.py
index 7756013684c..d61590c631b 100644
--- a/mne/io/mef/mef.py
+++ b/mne/io/mef/mef.py
@@ -50,9 +50,7 @@ def __init__(self, fname, password="", *, preload=False, verbose=None):
fname = _check_fname(fname, "read", True, "fname", need_dir=True)
# The dataset maybe have password
password = (
- (password or "").decode()
- if isinstance(password, bytes)
- else (password or "")
+ password.decode() if isinstance(password, bytes) else (password or "")
)
# Open the dataset
session = pymef.mef_session.MefSession(str(fname), password)
diff --git a/mne/io/nedf/nedf.py b/mne/io/nedf/nedf.py
index bd4054f1c16..031a8480435 100644
--- a/mne/io/nedf/nedf.py
+++ b/mne/io/nedf/nedf.py
@@ -6,6 +6,7 @@
from copy import deepcopy
from datetime import datetime, timezone
+from typing import Any
import numpy as np
@@ -119,7 +120,7 @@ def _parse_nedf_header(header):
n_samples = int(_getsubnodetext(eegset, "NumberOfRecordsOfEEG"))
n_full, n_last = divmod(n_samples, 5)
- dt_last = deepcopy(dt)
+ dt_last: list[Any] = deepcopy(dt)
assert dt_last[-1][-1] == (5,)
dt_last[-1] = list(dt_last[-1])
dt_last[-1][-1] = (n_last,)
diff --git a/mne/io/neuralynx/neuralynx.py b/mne/io/neuralynx/neuralynx.py
index 56ff9fa4adb..10c6b134ab3 100644
--- a/mne/io/neuralynx/neuralynx.py
+++ b/mne/io/neuralynx/neuralynx.py
@@ -137,7 +137,7 @@ def __init__(
info = create_info(
ch_types="seeg",
- ch_names=nlx_reader.header["signal_channels"]["name"].tolist(),
+ ch_names=nlx_reader.header["signal_channels"]["name"].tolist(), # ty: ignore # neo header
sfreq=nlx_reader.get_signal_sampling_rate(),
)
@@ -197,7 +197,7 @@ def __init__(
info["lowpass"] = np.min(lowpass_freqs)
# Neo reads only valid contiguous .ncs samples grouped as segments
- n_segments = nlx_reader.header["nb_segment"][0]
+ n_segments = nlx_reader.header["nb_segment"][0] # ty: ignore # neo header
block_id = 0 # assumes there's only one block of recording
# get segment start/stop times
@@ -418,7 +418,7 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult):
).T
all_data *= 1e-6 # Convert uV to V
- n_channels = len(nlx_reader.header["signal_channels"]["name"])
+ n_channels = len(nlx_reader.header["signal_channels"]["name"]) # ty: ignore # neo header
block = np.zeros((n_channels, stop - start), dtype=data.dtype)
block[idx] = all_data # shape = (n_channels, n_samples)
diff --git a/mne/io/nicolet/nicolet.py b/mne/io/nicolet/nicolet.py
index 05b8035bddd..fc2f0a45e25 100644
--- a/mne/io/nicolet/nicolet.py
+++ b/mne/io/nicolet/nicolet.py
@@ -5,6 +5,7 @@
import calendar
import datetime
from os import path
+from typing import Any
import numpy as np
@@ -53,7 +54,7 @@ def read_raw_nicolet(
Returns
-------
- raw : instance of Raw
+ raw : instance of RawNicolet
A Raw object containing the data.
See Also
@@ -82,7 +83,7 @@ def _get_nicolet_info(fname, ch_type, eog, ecg, emg, misc):
header = fname + ".head"
logger.info("Reading header...")
- header_info = dict()
+ header_info: dict[str, Any] = dict()
with open(header) as fid:
for line in fid:
var, value = line.split("=")
diff --git a/mne/io/nihon/nihon.py b/mne/io/nihon/nihon.py
index 6ef601c9591..2ea10c1d123 100644
--- a/mne/io/nihon/nihon.py
+++ b/mne/io/nihon/nihon.py
@@ -5,6 +5,7 @@
from collections import OrderedDict
from datetime import datetime, timezone
from pathlib import Path
+from typing import Any
import numpy as np
@@ -178,7 +179,7 @@ def _read_nihon_header(fname):
# Read the Nihon Kohden EEG file header
fname = _ensure_path(fname)
_chan_labels = _read_21e_file(fname)
- header = {}
+ header: dict[str, Any] = {}
logger.info(f"Reading header from {fname}")
with open(fname) as fid:
version = np.fromfile(fid, "|S16", 1).astype("U16")[0]
@@ -266,7 +267,7 @@ def _read_nihon_header(fname):
"I dont know how to read more than one "
"control block for this type of file :("
)
- if header["controlblocks"][0]["n_datablocks"] > 1:
+ if header["controlblocks"][0]["n_datablocks"] > 1: # ty: ignore[unsupported-operator]
# Multiple blocks, check that they all have the same kind of data
datablocks = header["controlblocks"][0]["datablocks"]
block_0 = datablocks[0]
diff --git a/mne/io/nirx/nirx.py b/mne/io/nirx/nirx.py
index 766986c4612..905d810e64f 100644
--- a/mne/io/nirx/nirx.py
+++ b/mne/io/nirx/nirx.py
@@ -319,7 +319,7 @@ def __init__(self, fname, saturated, *, preload=False, encoding=None, verbose=No
else:
inf = ConfigParser(allow_no_value=True)
inf.read(files["inf"])
- inf = inf._sections["Subject Demographics"]
+ inf = inf._sections["Subject Demographics"] # ty: ignore[unresolved-attribute]
# Store subject information from inf file in mne format
# Note: NIRX also records "Study Type", "Experiment History",
@@ -397,9 +397,11 @@ def __init__(self, fname, saturated, *, preload=False, encoding=None, verbose=No
# subset requested in the probe file
req_ind = np.array([], int)
for req_idx in range(requested_channels.shape[0]):
- sd_idx = np.where(
- (sources == requested_channels[req_idx][0])
- & (detectors == requested_channels[req_idx][1])
+ sd_idx = np.nonzero(
+ np.asarray(
+ (sources == requested_channels[req_idx][0])
+ & (detectors == requested_channels[req_idx][1])
+ )
)
req_ind = np.concatenate((req_ind, sd_idx[0]))
req_ind = req_ind.astype(int)
diff --git a/mne/io/nsx/nsx.py b/mne/io/nsx/nsx.py
index 1fc8a6968ea..59e7808ad6e 100644
--- a/mne/io/nsx/nsx.py
+++ b/mne/io/nsx/nsx.py
@@ -4,6 +4,7 @@
import os
from datetime import datetime, timezone
+from typing import Any
import numpy as np
@@ -113,7 +114,7 @@ def read_raw_nsx(
Returns
-------
- raw : instance of RawEDF
+ raw : instance of RawNSX
The raw instance.
See :class:`mne.io.Raw` for documentation of attributes and methods.
@@ -436,7 +437,7 @@ def _get_hdr_info(fname, stim_channel=True, eog=None, misc=None):
orig_format = ORIG_FORMAT
- raw_extras = {
+ raw_extras: dict[str, Any] = {
key: [r[key] for r in nsx_info["data_header"]]
for key in nsx_info["data_header"][0]
}
diff --git a/mne/io/persyst/persyst.py b/mne/io/persyst/persyst.py
index 8de44286445..68cefd00628 100644
--- a/mne/io/persyst/persyst.py
+++ b/mne/io/persyst/persyst.py
@@ -6,6 +6,7 @@
import os.path as op
from collections import OrderedDict
from datetime import datetime, timezone
+from typing import Any
import numpy as np
@@ -137,9 +138,9 @@ def __init__(self, fname, preload=False, verbose=None):
# get numerical metadata
# datatype is either 7 for 32 bit, or 0 for 16 bit
- datatype = fileinfo_dict.get("datatype")
- cal = float(fileinfo_dict.get("calibration"))
- n_chs = int(fileinfo_dict.get("waveformcount"))
+ datatype = fileinfo_dict["datatype"]
+ cal = float(fileinfo_dict["calibration"])
+ n_chs = int(fileinfo_dict["waveformcount"])
# Store subject information from lay file in mne format
# Note: Persyst also records "Physician", "Technician",
@@ -169,7 +170,7 @@ def __init__(self, fname, preload=False, verbose=None):
)
meas_date = None
else:
- testtime = datetime.strptime(patient_dict.get("testtime"), "%H:%M:%S")
+ testtime = datetime.strptime(patient_dict["testtime"], "%H:%M:%S")
meas_date = datetime(
year=testdate.year,
month=testdate.month,
@@ -305,7 +306,7 @@ def _get_subjectinfo(patient_dict):
birthdate = None
print(f"Unable to process birthdate of {birthdate} ")
- subject_info = {
+ subject_info: dict[str, Any] = {
"first_name": patient_dict.get("first"),
"middle_name": patient_dict.get("middle"),
"last_name": patient_dict.get("last"),
diff --git a/mne/io/snirf/_snirf.py b/mne/io/snirf/_snirf.py
index 36efaffa549..4ee83ba36ea 100644
--- a/mne/io/snirf/_snirf.py
+++ b/mne/io/snirf/_snirf.py
@@ -535,7 +535,7 @@ def natural_keys(text):
rpa=rpa,
hpi=hpi,
dig_ch_pos=extra_ps,
- coord_frame=_frame_to_str[coord_frame],
+ coord_frame=_frame_to_str[int(coord_frame)],
add_missing_fiducials=add_missing_fiducials,
)
else:
diff --git a/mne/tests/test_docstring_parameters.py b/mne/tests/test_docstring_parameters.py
index be255cf24d8..b63efc0be20 100644
--- a/mne/tests/test_docstring_parameters.py
+++ b/mne/tests/test_docstring_parameters.py
@@ -5,6 +5,8 @@
import importlib
import inspect
import re
+import types
+import typing
from pathlib import Path
from pkgutil import walk_packages
@@ -62,6 +64,14 @@
assert numpydoc_checks[0] == "all"
error_ignores = set(numpydoc_checks[1:])
+# The modules that ty type-checks strictly (see ``[tool.ty.src]`` in pyproject.toml);
+# here we also enforce that their type hints agree with the rendered docstrings.
+typed_modules = tuple(
+ path.removesuffix(".py").replace("/", ".")
+ for path in pyproject["tool"]["ty"]["src"]["include"]
+)
+assert typed_modules, "Could not find typed modules in [tool.ty.src] include"
+
def _func_name(func, cls=None):
"""Get the name."""
@@ -355,6 +365,117 @@ def test_documented():
)
+def _annotation_to_str(ann):
+ """Render a type annotation as a module-stripped string."""
+ origin = typing.get_origin(ann)
+ if origin in (typing.Union, getattr(types, "UnionType", None)):
+ return " | ".join(_annotation_to_str(a) for a in typing.get_args(ann))
+ if origin is not None: # e.g. list[Evoked], dict[str, int], tuple[int, ...]
+ args = typing.get_args(ann)
+ name = getattr(origin, "__name__", str(origin))
+ if args:
+ return f"{name}[{', '.join(_annotation_to_str(a) for a in args)}]"
+ return name
+ if isinstance(ann, str): # unevaluated forward reference, e.g. "EpochsFIF"
+ return ann
+ if ann is type(None):
+ return "None"
+ return getattr(ann, "__name__", str(ann))
+
+
+def _type_atoms(type_str):
+ """Reduce a type string to a canonical set of union members.
+
+ Handles both numpydoc docstring types (``instance of X``, ``A or B``,
+ ``list of X``) and annotation strings (``A | B``, ``list[X]``) so the two
+ can be compared, stripping any module qualifiers (``mne.evoked.Evoked`` and
+ ``numpy.ndarray`` become ``Evoked`` and ``ndarray``).
+ """
+ s = re.sub(r"\binstance of\b", "", type_str)
+ s = re.sub(r"\blist of (\w+)\b", r"list[\1]", s) # -> same shape as list[X]
+ s = s.replace(" or ", "|")
+ atoms = set()
+ for part in s.split("|"):
+ part = re.sub(r"[\w\.]*\.(\w+)", r"\1", part.strip()) # drop module paths
+ part = part.strip(" .,;:") # drop stray surrounding punctuation
+ if part:
+ atoms.add(part)
+ return atoms
+
+
+def _defined_under(obj, name):
+ """Return whether ``obj`` is defined in the ``name`` module or a submodule."""
+ module_name = inspect.getmodule(obj).__name__
+ return module_name == name or module_name.startswith(f"{name}.")
+
+
+def _check_type_hints(func, *, cls, where, incorrect):
+ """Compare a callable's type hints against its numpydoc docstring types."""
+ from numpydoc.docscrape import FunctionDoc
+
+ name = _func_name(func, cls)
+ sig = inspect.signature(func)
+ doc = FunctionDoc(func)
+ # documented types, keyed by parameter name (and "return")
+ doc_types = {p.name: p.type for p in doc["Parameters"] if p.name and p.type}
+ returns = [r.type for r in doc["Returns"] if r.type]
+ if len(returns) == 1: # skip multi-value (tuple) returns for simplicity
+ doc_types["return"] = returns[0]
+
+ checks = [
+ (pname, param.annotation)
+ for pname, param in sig.parameters.items()
+ if pname not in ("self", "cls")
+ and param.kind not in (param.VAR_POSITIONAL, param.VAR_KEYWORD)
+ and param.annotation is not inspect.Parameter.empty
+ ]
+ if sig.return_annotation is not inspect.Signature.empty:
+ checks.append(("return", sig.return_annotation))
+
+ for target, annotation in checks:
+ if target not in doc_types: # not documented (or not comparably); skip
+ continue
+ # ``Self`` describes the (sub)class, which docstrings spell out concretely
+ if _annotation_to_str(annotation) == "Self":
+ continue
+ ann_atoms = {a.lower() for a in _type_atoms(_annotation_to_str(annotation))}
+ doc_atoms = {d.lower() for d in _type_atoms(doc_types[target])}
+ if ann_atoms != doc_atoms:
+ incorrect.append(
+ f"{where} : {name} : {target} : type hint "
+ f"{sorted(ann_atoms)} != docstring {sorted(doc_atoms)}"
+ )
+
+
+@pytest.mark.slowtest
+def test_type_hints_match_docstrings():
+ """Test that type hints agree with numpydoc-rendered docstring types."""
+ pytest.importorskip("numpydoc")
+ pytest.importorskip("sklearn")
+
+ incorrect = []
+ for name in typed_modules:
+ module = __import__(name, globals())
+ for submod in name.split(".")[1:]:
+ module = getattr(module, submod)
+ for cname, cls in inspect.getmembers(module, inspect.isclass):
+ if cname.startswith("_") or not _defined_under(cls, name):
+ continue
+ for mname, method in inspect.getmembers(cls, inspect.isfunction):
+ if not mname.startswith("_"):
+ _check_type_hints(method, cls=cls, where=name, incorrect=incorrect)
+ for fname, func in inspect.getmembers(module, inspect.isfunction):
+ if not fname.startswith("_") and _defined_under(func, name):
+ _check_type_hints(func, cls=None, where=name, incorrect=incorrect)
+
+ incorrect = sorted(set(incorrect))
+ if incorrect:
+ raise AssertionError(
+ f"{len(incorrect)} type hint / docstring mismatch{_pl(incorrect)} "
+ f"found:\n" + "\n".join(incorrect)
+ )
+
+
def test_docdict_order():
"""Test that docdict is alphabetical."""
from mne.utils.docs import docdict
diff --git a/mne/transforms.py b/mne/transforms.py
index 605a9efa6a9..884e118c97d 100644
--- a/mne/transforms.py
+++ b/mne/transforms.py
@@ -55,7 +55,7 @@
ctf_meg=FIFF.FIFFV_MNE_COORD_CTF_DEVICE,
unknown=FIFF.FIFFV_COORD_UNKNOWN,
)
-_frame_to_str = {val: key for key, val in _str_to_frame.items()}
+_frame_to_str: dict[int, str] = {val: key for key, val in _str_to_frame.items()}
_verbose_frames = {
FIFF.FIFFV_COORD_UNKNOWN: "unknown",
diff --git a/mne/utils/_bunch.py b/mne/utils/_bunch.py
index 4fc5c454519..87a33bb51e4 100644
--- a/mne/utils/_bunch.py
+++ b/mne/utils/_bunch.py
@@ -5,6 +5,7 @@
# Copyright the MNE-Python contributors.
from copy import deepcopy
+from typing import Any
###############################################################################
# Create a Bunch class that acts like a struct (mybunch.key = val)
@@ -17,6 +18,21 @@ def __init__(self, **kwargs):
dict.__init__(self, kwargs)
self.__dict__ = self
+ def __getattr__(self, attr: str) -> Any:
+ # Keys are stored in the instance dict (``__dict__ is self``), so normal
+ # attribute access resolves them and this only runs for genuinely missing
+ # names. It exists to tell the type checker that arbitrary keys are
+ # accessible as attributes, with ``Any`` as the honest value type.
+ raise AttributeError(
+ f"{type(self).__name__!r} object has no attribute {attr!r}"
+ )
+
+ def __setattr__(self, attr: str, val: Any) -> None:
+ # Keys are set dynamically as attributes (e.g. ``bunch.foo = ...``); this
+ # pass-through just declares that to the type checker (it is exactly what
+ # ``object.__setattr__`` already does via ``Bunch.__dict__ is self``).
+ super().__setattr__(attr, val)
+
###############################################################################
# A protected version that prevents overwriting
@@ -32,24 +48,7 @@ def __setitem__(self, key, val): # noqa: D105
###############################################################################
-# A version that tweaks the __repr__ of its values based on keys
-
-
-class BunchConstNamed(BunchConst):
- """Class to provide nice __repr__ for our integer constants.
-
- Only supports string keys and int or float values.
- """
-
- def __setattr__(self, attr, val): # noqa: D105
- assert isinstance(attr, str)
- if isinstance(val, int):
- val = NamedInt(attr, val)
- elif isinstance(val, float):
- val = NamedFloat(attr, val)
- else:
- assert isinstance(val, BunchConstNamed), type(val)
- super().__setattr__(attr, val)
+# Named int/float subclasses used as the values of our constant bunches
class _Named:
@@ -102,3 +101,33 @@ class NamedFloat(_Named, float):
"""Float with a name in __repr__."""
pass # noqa
+
+
+###############################################################################
+# A version that tweaks the __repr__ of its values based on keys
+
+
+class BunchConstNamed(BunchConst):
+ """Class to provide nice __repr__ for our integer constants.
+
+ Only supports string keys and int or float values.
+ """
+
+ def __getattr__(self, attr: str) -> NamedInt | NamedFloat:
+ # Constants are populated dynamically and stored in the instance dict, so
+ # normal attribute access resolves them and this only runs for genuinely
+ # missing names. It exists mostly to tell the type checker the type of
+ # the dynamic constants (e.g. FIFF.FIFFV_ASPECT_AVERAGE).
+ raise AttributeError(
+ f"{type(self).__name__!r} object has no attribute {attr!r}"
+ )
+
+ def __setattr__(self, attr, val): # noqa: D105
+ assert isinstance(attr, str)
+ if isinstance(val, int):
+ val = NamedInt(attr, val)
+ elif isinstance(val, float):
+ val = NamedFloat(attr, val)
+ else:
+ assert isinstance(val, BunchConstNamed), type(val)
+ super().__setattr__(attr, val)
diff --git a/mne/utils/_typing.py b/mne/utils/_typing.py
index e1d5fdde107..e1f0f7de79c 100644
--- a/mne/utils/_typing.py
+++ b/mne/utils/_typing.py
@@ -9,6 +9,7 @@
if sys.version_info >= (3, 11):
from typing import Self
else:
- from typing import TypeVar
+ # TODO VERSION: Remove this when Python 3.11+ is required (use typing.Self)
+ from typing_extensions import Self
- Self = TypeVar("Self")
+__all__ = ["Self"]
diff --git a/mne/utils/config.py b/mne/utils/config.py
index 8b57fd837cf..3a120097a17 100644
--- a/mne/utils/config.py
+++ b/mne/utils/config.py
@@ -866,7 +866,6 @@ def sys_info(
"refleak",
"codespell",
"ipython",
- "mypy",
"pillow",
"pre-commit",
"ruff",
diff --git a/mne/utils/mixin.py b/mne/utils/mixin.py
index f0d4b9f7524..22f76619968 100644
--- a/mne/utils/mixin.py
+++ b/mne/utils/mixin.py
@@ -589,7 +589,7 @@ def tmax(self):
return self.times[-1]
@verbose
- def crop(self, tmin=None, tmax=None, include_tmax=True, verbose=None):
+ def crop(self, tmin=None, tmax=None, include_tmax=True, verbose=None) -> Self:
"""Crop data to a given time interval.
Parameters
@@ -648,7 +648,7 @@ def crop(self, tmin=None, tmax=None, include_tmax=True, verbose=None):
return self
@verbose
- def decimate(self, decim, offset=0, *, verbose=None):
+ def decimate(self, decim, offset=0, *, verbose=None) -> Self:
"""Decimate the time-series data.
Parameters
@@ -712,7 +712,7 @@ def decimate(self, decim, offset=0, *, verbose=None):
self._update_first_last()
return self
- def shift_time(self, tshift, relative=True):
+ def shift_time(self, tshift, relative=True) -> Self:
"""Shift time scale in epoched or evoked data.
Parameters
diff --git a/pyproject.toml b/pyproject.toml
index f84e22f8ced..6cc19e4b257 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,7 +3,7 @@ build-backend = "hatchling.build"
requires = ["hatch-vcs", "hatchling >= 1.27"]
[dependency-groups]
-dev = ["pip >= 25.1", "pyside6 >= 6.11.1", "rcssmin >= 1.1", {include-group = "doc"}, {include-group = "test_extra"}]
+dev = ["pip >= 25.1", "pre-commit", "pyside6 >= 6.11.1", "rcssmin >= 1.1", {include-group = "doc"}, {include-group = "test_extra"}]
# Dependencies for building the documentation
doc = [
"graphviz",
@@ -45,10 +45,8 @@ lockfile_extras = [
test = [
"codespell",
"ipython >= 8.20", # for testing notebook backend; also in "full-no-qt" and "doc"
- "mypy >= 0.14",
"numpydoc >= 1.6",
"pillow >= 10.2",
- "pre-commit",
"pytest >= 8.0, != 9.1.0", # https://github.com/pytest-dev/pytest/issues/14591
"pytest-cov >= 4.1",
"pytest-qt >= 4.3",
@@ -113,6 +111,7 @@ dependencies = [
"pooch >= 1.5",
"scipy >= 1.14", # released 2024-06-24, will become 1.15 on 2027-01-03
"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."
dynamic = ["version"]
@@ -247,43 +246,6 @@ exclude = [
raw-options = {version_scheme = "release-branch-semver"}
source = "vcs"
-[tool.mypy]
-# Avoid the conflict between mne/__init__.py and mne/__init__.pyi by ignoring the former
-exclude = '^mne/(beamformer|channels|commands|datasets|decoding|export|forward|gui|html_templates|inverse_sparse|io|minimum_norm|preprocessing|report|simulation|source_space|stats|time_frequency|utils|viz)?/?__init__\.py$'
-ignore_errors = true
-modules = ["mne"]
-scripts_are_modules = true
-strict = false
-
-[[tool.mypy.overrides]]
-ignore_missing_imports = true
-module = ['scipy.*']
-
-[[tool.mypy.overrides]]
-# Ignore "attr-defined" until we fix stuff like:
-# - BunchConstNamed: '"BunchConstNamed" has no attribute "FIFFB_EVOKED"'
-# - Missing __all__: 'Module "mne.io.snirf" does not explicitly export attribute "read_raw_snirf"'
-# Ignore "no-untyped-call" until we fix stuff like:
-# - 'Call to untyped function "end_block" in typed context'
-# Ignore "no-untyped-def" until we fix stuff like:
-# - 'Function is missing a type annotation'
-# Ignore "misc" until we fix stuff like:
-# - 'Cannot determine type of "_projector" in base class "ProjMixin"'
-# Ignore "assignment" until we fix stuff like:
-# - 'Incompatible types in assignment (expression has type "tuple[str, ...]", variable has type "str")'
-# Ignore "operator" until we fix stuff like:
-# - Unsupported operand types for - ("None" and "int")
-disable_error_code = [
- 'assignment',
- 'attr-defined',
- 'misc',
- 'no-untyped-call',
- 'no-untyped-def',
- 'operator',
-]
-ignore_errors = false
-module = ['mne.annotations', 'mne.epochs', 'mne.evoked', 'mne.io']
-
[tool.numpydoc_validation]
checks = [
"all", # report on all checks, except the below
@@ -501,6 +463,21 @@ directory = "other"
name = "Other changes"
showcontent = true
+[tool.ty.environment]
+python-version = "3.10" # minimum supported, keep in sync with requires-python
+
+# Files that are strictly type-checked with no rule overrides. A file is added
+# here only once it passes ty cleanly; grow this list as more of the codebase is
+# annotated and cleaned up.
+[tool.ty.src]
+exclude = ["mne/io/**/tests"]
+include = [
+ "mne/annotations.py",
+ "mne/epochs.py",
+ "mne/evoked.py",
+ "mne/io",
+]
+
[tool.vulture]
exclude = [
'conftest.py',
diff --git a/tools/check_pyproject_helpers.py b/tools/check_pyproject_helpers.py
index 1de1b9320a5..19277410cfb 100644
--- a/tools/check_pyproject_helpers.py
+++ b/tools/check_pyproject_helpers.py
@@ -21,7 +21,7 @@ def get_deps_to_check():
+ pyproject_data["project"]["dependencies"]
+ pyproject_data["dependency-groups"]["lockfile_extras"]
)
- n_want_deps = 12 # update when we add more core deps or auto-bumped pins!
+ n_want_deps = 13 # update when we add more core deps or auto-bumped pins!
assert len(check_deps) == n_want_deps, (
f"Number of dependencies being checked ({len(check_deps)=}) is not as "
f"expected {n_want_deps=}"
diff --git a/tools/github_actions_check_old_env.py b/tools/github_actions_check_old_env.py
index 7b4943bee17..7ed0e1bd4d0 100644
--- a/tools/github_actions_check_old_env.py
+++ b/tools/github_actions_check_old_env.py
@@ -30,7 +30,7 @@
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)
+ 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
diff --git a/tools/hooks/update_environment_file.py b/tools/hooks/update_environment_file.py
index b7fe21ad4ff..9c858ca70ad 100755
--- a/tools/hooks/update_environment_file.py
+++ b/tools/hooks/update_environment_file.py
@@ -29,6 +29,9 @@
"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):
diff --git a/tools/install_pre_requirements.sh b/tools/install_pre_requirements.sh
index 894f5e7114e..2ccec63e67e 100755
--- a/tools/install_pre_requirements.sh
+++ b/tools/install_pre_requirements.sh
@@ -61,7 +61,8 @@ python -m pip install $STD_ARGS \
git+https://github.com/BUNPC/pysnirf2 \
git+https://github.com/the-siesta-group/edfio \
trame trame-vtk trame-vuetify trame-pyvista nest-asyncio2 jupyter ipyevents ipympl \
- openmeeg imageio-ffmpeg xlrd mffpy traitlets pybv eeglabio defusedxml antio curryreader
+ openmeeg imageio-ffmpeg xlrd mffpy traitlets pybv eeglabio defusedxml antio curryreader \
+ filelock
echo "::endgroup::"
echo "::group::Make sure we're on a NumPy 2.0 variant"
diff --git a/tools/pylock.ci-old.toml b/tools/pylock.ci-old.toml
index ce3d975e03e..d933418eabf 100644
--- a/tools/pylock.ci-old.toml
+++ b/tools/pylock.ci-old.toml
@@ -16,42 +16,18 @@ version = "1.4.4"
sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", upload-time = 2020-05-11T07:59:51Z, size = 13470, hashes = { sha256 = "7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", upload-time = 2020-05-11T07:59:49Z, size = 9566, hashes = { sha256 = "a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" } }]
-[[packages]]
-name = "argparse"
-version = "1.4.0"
-sdist = { url = "https://files.pythonhosted.org/packages/18/dd/e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/argparse-1.4.0.tar.gz", upload-time = 2015-09-12T20:22:16Z, size = 70508, hashes = { sha256 = "62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/f2/94/3af39d34be01a24a6e65433d19e107099374224905f1e0cc6bbe1fd22a2f/argparse-1.4.0-py2.py3-none-any.whl", upload-time = 2015-09-14T16:03:16Z, size = 23000, hashes = { sha256 = "c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314" } }]
-
-[[packages]]
-name = "aspy-yaml"
-version = "1.3.0"
-sdist = { url = "https://files.pythonhosted.org/packages/a1/e9/2ee775d3e66319e08135505a1dd3cdba606b4da4caeb617eb3514d901b14/aspy.yaml-1.3.0.tar.gz", upload-time = 2019-05-23T18:32:02Z, size = 2998, hashes = { sha256 = "e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/99/ce/78be097b00817ccf02deaf481eb7a603eecee6fa216e82fa7848cd265449/aspy.yaml-1.3.0-py2.py3-none-any.whl", upload-time = 2019-05-23T18:32:00Z, size = 3453, hashes = { sha256 = "463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc" } }]
-
[[packages]]
name = "asttokens"
version = "3.0.1"
sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", upload-time = 2025-11-15T16:43:48Z, size = 62308, hashes = { sha256 = "71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", upload-time = 2025-11-15T16:43:16Z, size = 27047, hashes = { sha256 = "15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a" } }]
-[[packages]]
-name = "attrs"
-version = "25.4.0"
-sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", upload-time = 2025-10-06T13:54:44Z, size = 934251, hashes = { sha256 = "16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", upload-time = 2025-10-06T13:54:43Z, size = 67615, hashes = { sha256 = "adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373" } }]
-
[[packages]]
name = "babel"
version = "2.18.0"
sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", upload-time = 2026-02-01T12:30:56Z, size = 9959554, hashes = { sha256 = "b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", upload-time = 2026-02-01T12:30:53Z, size = 10196845, hashes = { sha256 = "e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35" } }]
-[[packages]]
-name = "cached-property"
-version = "2.0.1"
-sdist = { url = "https://files.pythonhosted.org/packages/76/4b/3d870836119dbe9a5e3c9a61af8cc1a8b69d75aea564572e385882d5aefb/cached_property-2.0.1.tar.gz", upload-time = 2024-10-25T15:43:55Z, size = 10574, hashes = { sha256 = "484d617105e3ee0e4f1f58725e72a8ef9e93deee462222dbd51cd91230897641" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/11/0e/7d8225aab3bc1a0f5811f8e1b557aa034ac04bdf641925b30d3caf586b28/cached_property-2.0.1-py3-none-any.whl", upload-time = 2024-10-25T15:43:54Z, size = 7428, hashes = { sha256 = "f617d70ab1100b7bcf6e42228f9ddcb78c676ffa167278d9f730d1c2fba69ccb" } }]
-
[[packages]]
name = "certifi"
version = "2026.1.4"
@@ -93,24 +69,12 @@ version = "0.12.1"
sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", upload-time = 2023-10-07T05:32:18Z, size = 7615, hashes = { sha256 = "88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", upload-time = 2023-10-07T05:32:16Z, size = 8321, hashes = { sha256 = "85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30" } }]
-[[packages]]
-name = "dbutils"
-version = "3.1.2"
-sdist = { url = "https://files.pythonhosted.org/packages/55/33/b739e991d8234b3fb0602d501b4aacda88ad1fd5d061b5b9d6022fa35ed6/dbutils-3.1.2.tar.gz", upload-time = 2025-09-07T17:01:31Z, size = 95869, hashes = { sha256 = "160b5788154f1adeddc61080daff1530b4df2ba0d45af1c3bfbac76db24186b3" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/98/a7/4fe7da3082241028e62c390bf9357d60522dd03d9329e3a560045fe14dfd/dbutils-3.1.2-py3-none-any.whl", upload-time = 2025-09-07T17:01:30Z, size = 32936, hashes = { sha256 = "0cb388a89eeecf04089aef113a7007c3fac9199e9580c8549829f954870c403a" } }]
-
[[packages]]
name = "decorator"
version = "5.1.0"
sdist = { url = "https://files.pythonhosted.org/packages/92/3c/34f8448b61809968052882b830f7d8d9a8e1c07048f70deb039ae599f73c/decorator-5.1.0.tar.gz", upload-time = 2021-09-11T05:30:17Z, size = 34900, hashes = { sha256 = "e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/3d/cc/d7b758e54779f7e465179427de7e78c601d3330d6c411ea7ba9ae2f38102/decorator-5.1.0-py3-none-any.whl", upload-time = 2021-09-11T05:30:14Z, size = 9061, hashes = { sha256 = "7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374" } }]
-[[packages]]
-name = "distlib"
-version = "0.4.0"
-sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", upload-time = 2025-07-17T16:52:00Z, size = 614605, hashes = { sha256 = "feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", upload-time = 2025-07-17T16:51:58Z, size = 469047, hashes = { sha256 = "9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16" } }]
-
[[packages]]
name = "docutils"
version = "0.19"
@@ -130,13 +94,6 @@ version = "2.2.1"
sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", upload-time = 2025-09-01T09:48:10Z, size = 1129488, hashes = { sha256 = "3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", upload-time = 2025-09-01T09:48:08Z, size = 28317, hashes = { sha256 = "760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017" } }]
-[[packages]]
-name = "filelock"
-version = "3.20.3"
-marker = "python_full_version >= '3.10'"
-sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", upload-time = 2026-01-09T17:55:05Z, size = 19485, hashes = { sha256 = "18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", upload-time = 2026-01-09T17:55:04Z, size = 16701, hashes = { sha256 = "4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1" } }]
-
[[packages]]
name = "fonttools"
version = "4.61.1"
@@ -146,11 +103,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", upload-time = 2025-12-12T17:31:21Z, size = 1148996, hashes = { sha256 = "17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371" } },
]
-[[packages]]
-name = "hmako"
-version = "1.16"
-sdist = { url = "https://files.pythonhosted.org/packages/b7/99/f2b5f60650d398dd2bbb67e4c097b64ad645e10c46b72053af7e1e4be5c7/hmako-1.16.tar.gz", upload-time = 2013-02-24T11:03:49Z, size = 206105, hashes = { sha256 = "b5f2f0415dce2c10aff23762c0b33a20fe396d299090a37410782a21efaaf53a" } }
-
[[packages]]
name = "idna"
version = "3.11"
@@ -193,18 +145,6 @@ version = "1.5.3"
sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", upload-time = 2025-12-15T08:41:46Z, size = 331603, hashes = { sha256 = "8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", upload-time = 2025-12-15T08:41:44Z, size = 309071, hashes = { sha256 = "5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713" } }]
-[[packages]]
-name = "jsonschema"
-version = "4.26.0"
-sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", upload-time = 2026-01-07T13:41:07Z, size = 366583, hashes = { sha256 = "0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", upload-time = 2026-01-07T13:41:05Z, size = 90630, hashes = { sha256 = "d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce" } }]
-
-[[packages]]
-name = "jsonschema-specifications"
-version = "2025.9.1"
-sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", upload-time = 2025-09-08T01:34:59Z, size = 32855, hashes = { sha256 = "b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", upload-time = 2025-09-08T01:34:57Z, size = 18437, hashes = { sha256 = "98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe" } }]
-
[[packages]]
name = "kiwisolver"
version = "1.4.9"
@@ -217,12 +157,6 @@ version = "0.3"
sdist = { url = "https://files.pythonhosted.org/packages/0e/3a/1630a735bfdf9eb857a3b9a53317a1e1658ea97a1b4b39dcb0f71dae81f8/lazy_loader-0.3.tar.gz", upload-time = 2023-06-30T21:12:55Z, size = 12268, hashes = { sha256 = "3b68898e34f5b2a29daaaac172c6555512d0f32074f147e2254e4a6d9d838f37" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/a1/c3/65b3814e155836acacf720e5be3b5757130346670ac454fee29d3eda1381/lazy_loader-0.3-py3-none-any.whl", upload-time = 2023-06-30T21:12:51Z, size = 9087, hashes = { sha256 = "1e9e76ee8631e264c62ce10006718e80b2cfc74340d17d1031e0f84af7478554" } }]
-[[packages]]
-name = "mako"
-version = "1.3.10"
-sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", upload-time = 2025-04-10T12:44:31Z, size = 392474, hashes = { sha256 = "99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", upload-time = 2025-04-10T12:50:53Z, size = 78509, hashes = { sha256 = "baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59" } }]
-
[[packages]]
name = "markupsafe"
version = "3.0.3"
@@ -241,17 +175,6 @@ version = "0.2.1"
sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", upload-time = 2025-10-23T09:00:22Z, size = 8110, hashes = { sha256 = "e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", upload-time = 2025-10-23T09:00:20Z, size = 9516, hashes = { sha256 = "d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76" } }]
-[[packages]]
-name = "mypy"
-version = "0.14"
-sdist = { url = "https://files.pythonhosted.org/packages/ad/4b/629308f39cd1eb6176fa009e028f8c73d79223baf12195a2f2bc195d1f2b/mypy-0.14.tar.gz", upload-time = 2009-09-16T06:24:28Z, size = 863929, hashes = { sha256 = "e578477db9f723968d72199625c282110f89ce24334405ff0c5a8d5beb267b9f" } }
-
-[[packages]]
-name = "nodeenv"
-version = "1.10.0"
-sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", upload-time = 2025-12-20T14:08:54Z, size = 55611, hashes = { sha256 = "996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", upload-time = 2025-12-20T14:08:52Z, size = 23438, hashes = { sha256 = "5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827" } }]
-
[[packages]]
name = "numpy"
version = "2.0.0"
@@ -264,11 +187,6 @@ version = "1.6.0"
sdist = { url = "https://files.pythonhosted.org/packages/5f/ed/5ca4b2e90f4b0781f5fac49cdb2947cf719b6d289eedb67e8b1a63d019e3/numpydoc-1.6.0.tar.gz", upload-time = 2023-09-26T00:24:33Z, size = 85475, hashes = { sha256 = "ae7a5380f0a06373c3afe16ccd15bd79bc6b07f2704cbc6f1e7ecc94b4f5fc0d" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/9c/94/09c437fd4a5fb5adf0468c0865c781dbc11d399544b55f1163d5d4414afb/numpydoc-1.6.0-py3-none-any.whl", upload-time = 2023-09-26T00:24:32Z, size = 61722, hashes = { sha256 = "b6ddaa654a52bdf967763c1e773be41f1c3ae3da39ee0de973f2680048acafaa" } }]
-[[packages]]
-name = "ordereddict"
-version = "1.1"
-sdist = { url = "https://files.pythonhosted.org/packages/53/25/ef88e8e45db141faa9598fbf7ad0062df8f50f881a36ed6a0073e1572126/ordereddict-1.1.tar.gz", upload-time = 2010-10-28T10:08:17Z, size = 2114, hashes = { sha256 = "1c35b4ac206cef2d24816c89f89cf289dd3d38cf7c449bb3fab7bf6d43f01b1f" } }
-
[[packages]]
name = "packaging"
version = "20.0"
@@ -287,12 +205,6 @@ version = "0.8.5"
sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", upload-time = 2025-08-23T15:15:28Z, size = 401205, hashes = { sha256 = "034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", upload-time = 2025-08-23T15:15:25Z, size = 106668, hashes = { sha256 = "646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887" } }]
-[[packages]]
-name = "paste"
-version = "3.10.1"
-sdist = { url = "https://files.pythonhosted.org/packages/d7/1c/6bc9040bf9b4cfc9334f66d2738f952384c106c48882adf6097fed3da966/paste-3.10.1.tar.gz", upload-time = 2024-05-01T11:41:08Z, size = 652629, hashes = { sha256 = "1c3d12065a5e8a7a18c0c7be1653a97cf38cc3e9a5a0c8334a9dd992d3a05e4a" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/2e/14/032895c25726a859bf48b8ed68944c3efc7a3decd920533ed929f12f08a1/Paste-3.10.1-py3-none-any.whl", upload-time = 2024-05-01T11:41:05Z, size = 289253, hashes = { sha256 = "995e9994b6a94a2bdd8bd9654fb70ca3946ffab75442468bacf31b4d06481c3d" } }]
-
[[packages]]
name = "pexpect"
version = "4.9.0"
@@ -315,35 +227,18 @@ version = "1.12.1.2"
sdist = { url = "https://files.pythonhosted.org/packages/24/03/e26bf3d6453b7fda5bd2b84029a426553bb373d6277ef6b5ac8863421f87/pkginfo-1.12.1.2.tar.gz", upload-time = 2025-02-19T15:27:37Z, size = 451828, hashes = { sha256 = "5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/fa/3d/f4f2ba829efb54b6cd2d91349c7463316a9cc55a43fc980447416c88540f/pkginfo-1.12.1.2-py3-none-any.whl", upload-time = 2025-02-19T15:27:33Z, size = 32717, hashes = { sha256 = "c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343" } }]
-[[packages]]
-name = "platformdirs"
-version = "4.5.1"
-sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", upload-time = 2025-12-05T13:52:58Z, size = 21715, hashes = { sha256 = "61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", upload-time = 2025-12-05T13:52:56Z, size = 18731, hashes = { sha256 = "d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31" } }]
-
[[packages]]
name = "pluggy"
version = "1.6.0"
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", upload-time = 2025-05-15T12:30:07Z, size = 69412, hashes = { sha256 = "7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", upload-time = 2025-05-15T12:30:06Z, size = 20538, hashes = { sha256 = "e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" } }]
-[[packages]]
-name = "plumbum"
-version = "1.10.0"
-sdist = { url = "https://files.pythonhosted.org/packages/dc/c8/11a5f792704b70f071a3dbc329105a98e9cc8d25daaf09f733c44eb0ef8e/plumbum-1.10.0.tar.gz", upload-time = 2025-10-31T05:02:48Z, size = 320039, hashes = { sha256 = "f8cbf0ecec0b73ff4e349398b65112a9e3f9300e7dc019001217dcc148d5c97c" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.0-py3-none-any.whl", upload-time = 2025-10-31T05:02:47Z, size = 127383, hashes = { sha256 = "9583d737ac901c474d99d030e4d5eec4c4e6d2d7417b1cf49728cf3be34f6dc8" } }]
-
[[packages]]
name = "pooch"
version = "1.5.0"
sdist = { url = "https://files.pythonhosted.org/packages/01/b8/aa2e51af6af8879fc9e61d75a31d16946c71b0abd07bde4ec2d99338fae5/pooch-1.5.0.tar.gz", upload-time = 2021-08-23T09:46:03Z, size = 60596, hashes = { sha256 = "9eaf36caccb78fbf1fd42acbeda9890b81de4d7e5823686ba49dc3010c21e1cf" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/1a/57/03a879380fe8b501e25617ea59a51a390ee9a5a09769945e8cb2c21ecaf1/pooch-1.5.0-py3-none-any.whl", upload-time = 2021-08-23T09:46:01Z, size = 56412, hashes = { sha256 = "54c9321a1cab8cd056ced497649eac69891066038319b31bebf13ec8ac79862b" } }]
-[[packages]]
-name = "pre-commit"
-version = "0.2.0"
-sdist = { url = "https://files.pythonhosted.org/packages/3f/cd/7fef5156196d981d4c4d0271842c0a1f0fc5d76831abfca3e858f111d2e9/pre_commit-0.2.0.tar.gz", upload-time = 2014-06-17T17:04:14Z, size = 16234, hashes = { sha256 = "21328d65ad427fbb8455d36d9993c696ce7ba4361cd3b41baf111acfd81b41fe" } }
-
[[packages]]
name = "prompt-toolkit"
version = "3.0.52"
@@ -416,18 +311,6 @@ version = "2025.2"
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", upload-time = 2025-03-25T02:25:00Z, size = 320884, hashes = { sha256 = "360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", upload-time = 2025-03-25T02:24:58Z, size = 509225, hashes = { sha256 = "5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00" } }]
-[[packages]]
-name = "pyyaml"
-version = "6.0.3"
-sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", upload-time = 2025-09-25T21:33:16Z, size = 130960, hashes = { sha256 = "d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2025-09-25T21:31:51Z, size = 770293, hashes = { sha256 = "9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b" } }]
-
-[[packages]]
-name = "referencing"
-version = "0.37.0"
-sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", upload-time = 2025-10-13T15:30:48Z, size = 78036, hashes = { sha256 = "44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", upload-time = 2025-10-13T15:30:47Z, size = 26766, hashes = { sha256 = "381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231" } }]
-
[[packages]]
name = "refleak"
version = "0.1.1"
@@ -440,12 +323,6 @@ version = "2.32.5"
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", upload-time = 2025-08-18T20:46:02Z, size = 134517, hashes = { sha256 = "dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", upload-time = 2025-08-18T20:46:00Z, size = 64738, hashes = { sha256 = "2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6" } }]
-[[packages]]
-name = "rpds-py"
-version = "0.30.0"
-sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", upload-time = 2025-11-30T20:24:38Z, size = 69469, hashes = { sha256 = "dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-11-30T20:21:42Z, size = 390522, hashes = { sha256 = "0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3" } }]
-
[[packages]]
name = "ruff"
version = "0.1.0"
@@ -464,21 +341,6 @@ version = "1.14.0"
sdist = { url = "https://files.pythonhosted.org/packages/4e/e5/0230da034a2e1b1feb32621d7cd57c59484091d6dccc9e6b855b0d309fc9/scipy-1.14.0.tar.gz", upload-time = 2024-06-24T20:35:18Z, size = 58618870, hashes = { sha256 = "b5923f48cb840380f9854339176ef21763118a7300a88203ccd0bdd26e58527b" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/e2/20/15c8fe0dfebb6facd81b3d08bf45dfa080e305deb17172b0a40eba59e927/scipy-1.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-06-24T20:32:25Z, size = 41135959, hashes = { sha256 = "42470ea0195336df319741e230626b6225a740fd9dce9642ca13e98f667047c0" } }]
-[[packages]]
-name = "setuptools"
-version = "80.10.2"
-sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", upload-time = 2026-01-25T22:38:17Z, size = 1200343, hashes = { sha256 = "8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", upload-time = 2026-01-25T22:38:15Z, size = 1064234, hashes = { sha256 = "95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173" } }]
-
-[[packages]]
-name = "simplejson"
-version = "3.20.2"
-sdist = { url = "https://files.pythonhosted.org/packages/41/f4/a1ac5ed32f7ed9a088d62a59d410d4c204b3b3815722e2ccfb491fa8251b/simplejson-3.20.2.tar.gz", upload-time = 2025-09-26T16:29:36Z, size = 85784, hashes = { sha256 = "5fe7a6ce14d1c300d80d08695b7f7e633de6cd72c80644021874d985b3393649" } }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c1/b9/c54eef4226c6ac8e9a389bbe5b21fef116768f97a2dc1a683c716ffe66ef/simplejson-3.20.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2025-09-26T16:27:36Z, size = 138172, hashes = { sha256 = "3a97249ee1aee005d891b5a211faf58092a309f3d9d440bc269043b08f662eda" } },
- { url = "https://files.pythonhosted.org/packages/05/5b/83e1ff87eb60ca706972f7e02e15c0b33396e7bdbd080069a5d1b53cf0d8/simplejson-3.20.2-py3-none-any.whl", upload-time = 2025-09-26T16:29:35Z, size = 57309, hashes = { sha256 = "3b6bb7fb96efd673eac2e4235200bfffdc2353ad12c54117e1e4e2fc485ac017" } },
-]
-
[[packages]]
name = "six"
version = "1.17.0"
@@ -533,11 +395,6 @@ version = "2.0.0"
sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", upload-time = 2024-07-29T01:10:09Z, size = 16080, hashes = { sha256 = "e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", upload-time = 2024-07-29T01:10:08Z, size = 92072, hashes = { sha256 = "6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331" } }]
-[[packages]]
-name = "sqlbean"
-version = "0.603"
-sdist = { url = "https://files.pythonhosted.org/packages/14/54/162f7bdecd7bb18d3329c9cd7eb4ea26a50906c48318b20b564bbdfdbae9/sqlbean-0.603.tar.gz", upload-time = 2012-02-11T05:34:38Z, size = 18587, hashes = { sha256 = "eb5137f84508ba50c4e1df2176781d35a2dc4bf15b09181ba5059b781032d8ce" } }
-
[[packages]]
name = "stack-data"
version = "0.6.3"
@@ -550,12 +407,6 @@ version = "0.9.0"
sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", upload-time = 2022-10-06T17:21:48Z, size = 81090, hashes = { sha256 = "0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", upload-time = 2022-10-06T17:21:44Z, size = 35252, hashes = { sha256 = "024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f" } }]
-[[packages]]
-name = "tempita"
-version = "0.6.0"
-sdist = { url = "https://files.pythonhosted.org/packages/74/d5/ad8a832e120cb9b743f175b80368b457a1078cd65bd9b8578aca5ddb1bd2/tempita-0.6.0.tar.gz", upload-time = 2024-11-12T21:55:45Z, size = 19205, hashes = { sha256 = "76e15de0137e5011c22949cc15a5161f623fe6a31655751c6d765db6bbac27b6" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/23/05/346f980c4e643f04ec3630c20d8169ffbea5137f6dcf7619d7349f7f9401/Tempita-0.6.0-py3-none-any.whl", upload-time = 2024-11-12T21:55:43Z, size = 13752, hashes = { sha256 = "696160ba5302b344934558d9d4c94a7b9778b7641612f4e20d62fcac4d7a02c9" } }]
-
[[packages]]
name = "threadpoolctl"
version = "3.6.0"
@@ -590,7 +441,7 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/da/07/baf5870c94b669c
[[packages]]
name = "typing-extensions"
version = "4.15.0"
-marker = "python_full_version < '3.13'"
+marker = "python_full_version < '3.11'"
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", upload-time = 2025-08-25T13:49:26Z, size = 109391, hashes = { sha256 = "0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", upload-time = 2025-08-25T13:49:24Z, size = 44614, hashes = { sha256 = "f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" } }]
@@ -606,12 +457,6 @@ version = "2.6.3"
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", upload-time = 2026-01-07T16:24:43Z, size = 435556, hashes = { sha256 = "1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", upload-time = 2026-01-07T16:24:42Z, size = 131584, hashes = { sha256 = "bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4" } }]
-[[packages]]
-name = "virtualenv"
-version = "20.36.1"
-sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", upload-time = 2026-01-09T18:21:01Z, size = 6032239, hashes = { sha256 = "8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", upload-time = 2026-01-09T18:20:59Z, size = 6008258, hashes = { sha256 = "575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f" } }]
-
[[packages]]
name = "vulture"
version = "0.1"
@@ -622,14 +467,3 @@ name = "wcwidth"
version = "0.5.3"
sdist = { url = "https://files.pythonhosted.org/packages/c2/62/a7c072fbfefb2980a00f99ca994279cb9ecf310cb2e6b2a4d2a28fe192b3/wcwidth-0.5.3.tar.gz", upload-time = 2026-01-31T03:52:10Z, size = 157587, hashes = { sha256 = "53123b7af053c74e9fe2e92ac810301f6139e64379031f7124574212fb3b4091" } }
wheels = [{ url = "https://files.pythonhosted.org/packages/3c/c1/d73f12f8cdb1891334a2ccf7389eed244d3941e74d80dd220badb937f3fb/wcwidth-0.5.3-py3-none-any.whl", upload-time = 2026-01-31T03:52:09Z, size = 92981, hashes = { sha256 = "d584eff31cd4753e1e5ff6c12e1edfdb324c995713f75d26c29807bb84bf649e" } }]
-
-[[packages]]
-name = "weberror"
-version = "0.13.1"
-sdist = { url = "https://files.pythonhosted.org/packages/07/0a/09ca5eb0fab5c0d17b380026babe81c96ecebb13f2b06c3203432dd7be72/WebError-0.13.1.tar.gz", upload-time = 2016-04-10T01:48:48Z, size = 85910, hashes = { sha256 = "c19f8bd57de2f1eea1b18a44f1ba1ad27421097c9ecfa0ae754fa42c9cdd9864" } }
-
-[[packages]]
-name = "webob"
-version = "1.8.9"
-sdist = { url = "https://files.pythonhosted.org/packages/85/0b/1732085540b01f65e4e7999e15864fe14cd18b12a95731a43fd6fd11b26a/webob-1.8.9.tar.gz", upload-time = 2024-10-24T03:19:20Z, size = 279775, hashes = { sha256 = "ad6078e2edb6766d1334ec3dee072ac6a7f95b1e32ce10def8ff7f0f02d56589" } }
-wheels = [{ url = "https://files.pythonhosted.org/packages/50/bd/c336448be43d40be28e71f2e0f3caf7ccb28e2755c58f4c02c065bfe3e8e/WebOb-1.8.9-py2.py3-none-any.whl", upload-time = 2024-10-24T03:19:18Z, size = 115364, hashes = { sha256 = "45e34c58ed0c7e2ecd238ffd34432487ff13d9ad459ddfd77895e67abba7c1f9" } }]