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" } }]