From de0a39382515c085e4a4eb7599447ff7718417c5 Mon Sep 17 00:00:00 2001 From: Hamza Abdelhedi Date: Sun, 30 Aug 2026 16:42:32 -0400 Subject: [PATCH 01/11] MAINT modernize developer tooling --- .pre-commit-config.yaml | 28 +++++------ .spin/cmds.py | 88 ++++++++++++++++++++++++++++++++++ .yamllint.yml | 9 ++++ codecov.yml | 4 +- mne_denoise/_logging.py | 8 ++-- pyproject.toml | 46 +++++++++--------- scripts/check_dist.py | 12 +++-- scripts/rename_towncrier.py | 94 +++++++++++++++++++++++++++++++++++++ tools/pylock.ci-old.toml | 48 +++++++++---------- 9 files changed, 265 insertions(+), 72 deletions(-) create mode 100644 .spin/cmds.py create mode 100644 .yamllint.yml create mode 100644 scripts/rename_towncrier.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 41d44890..514a94de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,14 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.16.0 + rev: aab412d509121cb5f7533134b7e67f9fab59c682 # frozen: v0.16.4 hooks: - - id: ruff + - id: ruff-check args: [--fix, --exit-non-zero-on-fix] - id: ruff-format types_or: [python, pyi, jupyter, markdown] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 hooks: - id: check-added-large-files args: [--maxkb=1000] @@ -25,20 +25,20 @@ repos: - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.1.0 + - repo: https://github.com/adrienverge/yamllint + rev: cba56bcde1fdd01c1deb3f945e69764c291a6530 # frozen: v1.38.0 hooks: - - id: prettier - types_or: [yaml, json] - exclude: ^docs/ + - id: yamllint + args: [--strict, -c, .yamllint.yml] + + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: 451b56af716f9f0d0c2b816503a3fd0cf8b036fa # frozen: v1.29.0 + hooks: + - id: zizmor + args: [--no-progress, --min-severity=medium] - repo: https://github.com/codespell-project/codespell - rev: v2.2.6 + rev: 57b21406f092110c18776e39b0bda50d37c945c8 # frozen: v2.4.3 hooks: - id: codespell - additional_dependencies: [tomli] args: [--skip, "*.html,*.css,*.js,*.svg,*.lock,*.ipynb"] - -ci: - autoupdate_schedule: quarterly - skip: [codespell] diff --git a/.spin/cmds.py b/.spin/cmds.py new file mode 100644 index 00000000..974a1ae0 --- /dev/null +++ b/.spin/cmds.py @@ -0,0 +1,88 @@ +"""Project commands exposed through Spin.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +import click + + +def _run( + *command: str, + args: tuple[str, ...] = (), + env: dict[str, str] | None = None, +) -> None: + """Run a project command and propagate its exit status.""" + subprocess.run([*command, *args], check=True, env=env) + + +@click.command(context_settings={"ignore_unknown_options": True}) +@click.argument("args", nargs=-1, type=click.UNPROCESSED) +def test(args: tuple[str, ...]) -> None: + """Run the test suite, optionally forwarding pytest arguments.""" + _run("pytest", "-q", args=args) + + +@click.command() +def lint() -> None: + """Run all repository hooks.""" + _run("prek", "run", "--all-files") + + +@click.command() +def docs() -> None: + """Build the documentation with warnings treated as errors.""" + with tempfile.TemporaryDirectory(prefix="mne-denoise-docs-") as temp_dir: + temp_root = Path(temp_dir) + environment = os.environ.copy() + environment.update( + { + "MPLBACKEND": "Agg", + "MPLCONFIGDIR": str(temp_root / "mplconfig"), + "HOME": str(temp_root / "home"), + "MNE_HOME": str(temp_root / "mne"), + "NUMBA_CACHE_DIR": str(temp_root / "numba"), + "MNE_DONTWRITE_HOME": "true", + } + ) + for directory in ("mplconfig", "home", "mne", "numba"): + (temp_root / directory).mkdir() + _run( + sys.executable, + "-m", + "sphinx", + "-b", + "html", + "-W", + "--keep-going", + "docs", + "docs/_build/html", + env=environment, + ) + + +@click.command() +def build() -> None: + """Build and validate clean Python distribution artifacts.""" + dist = Path("dist") + if dist.is_dir(): + shutil.rmtree(dist) + elif dist.exists(): + dist.unlink() + _run(sys.executable, "-m", "build") + artifacts = tuple(str(path) for path in sorted(dist.iterdir())) + _run(sys.executable, "-m", "twine", "check", "--strict", args=artifacts) + _run(sys.executable, "scripts/check_dist.py", "dist") + + +@click.command() +def check() -> None: + """Run hooks, tests, and distribution validation.""" + _run("spin", "lint") + _run("spin", "test") + _run("spin", "build") diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 00000000..78b78dd2 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,9 @@ +extends: default + +rules: + line-length: disable + document-start: disable + new-lines: + type: platform + indentation: + indent-sequences: consistent diff --git a/codecov.yml b/codecov.yml index bd868fff..088d1892 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,8 +2,8 @@ coverage: status: project: default: - target: 80% # Global target - threshold: 1% # Allow 1% drop + target: 80% # Global target + threshold: 1% # Allow 1% drop patch: default: # New code should be well covered, but need not exactly match the diff --git a/mne_denoise/_logging.py b/mne_denoise/_logging.py index 660b0b32..da10a75f 100644 --- a/mne_denoise/_logging.py +++ b/mne_denoise/_logging.py @@ -16,7 +16,7 @@ from contextvars import ContextVar from functools import wraps from numbers import Integral -from typing import Any, TypeVar +from typing import Any logger = logging.getLogger("mne_denoise") @@ -26,8 +26,6 @@ default=_UNSET, ) -_F = TypeVar("_F", bound=Callable[..., Any]) - def _level_from_verbose(verbose: bool | str | int | None) -> int | None: """Resolve one MNE-style verbosity value to a logging level.""" @@ -73,7 +71,7 @@ def use_log_level(verbose: bool | str | int | None) -> Iterator[None]: _active_verbose_scope.reset(token) -def verbose(function: _F) -> _F: +def verbose[**P, T](function: Callable[P, T]) -> Callable[P, T]: """Decorate a public operation with a temporary ``verbose`` override. The decorator accepts the same forms as MNE-Python's ``@verbose``. An @@ -98,4 +96,4 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: return function(*args, **kwargs) - return wrapper # type: ignore[return-value] + return wrapper diff --git a/pyproject.toml b/pyproject.toml index d3ffc9bb..d534f572 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "mne-denoise" dynamic = ["version"] description = "Artifact removal and signal denoising for EEG and MEG." readme = "README.md" -requires-python = ">=3.11" +requires-python = ">=3.12" license = "BSD-3-Clause" license-files = ["LICENSE"] authors = [ @@ -37,7 +37,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", @@ -80,7 +79,7 @@ test = [ "pytest>=7.4.0", "pytest-cov>=4.1.0", "pytest-timeout>=2.2.0", - "pandas>=1.5", + "pandas>=2.1.1", "seaborn>=0.12", { include-group = "lockfile_extras" }, ] @@ -103,12 +102,8 @@ build = [ ] lint = [ - "ruff>=0.16.0", - "pre-commit>=3.6.0", -] - -typecheck = [ - "mypy>=1.8.0", + "prek>=0.5", + "spin>=0.18", ] changelog = [ @@ -121,7 +116,6 @@ dev = [ { include-group = "doc" }, { include-group = "build" }, { include-group = "lint" }, - { include-group = "typecheck" }, { include-group = "changelog" }, ] @@ -131,7 +125,7 @@ raw-options = { version_scheme = "guess-next-dev" } [tool.ruff] line-length = 88 -target-version = "py311" +target-version = "py312" [tool.ruff.lint] select = [ @@ -199,18 +193,9 @@ exclude_lines = [ "raise NotImplementedError", ] -[tool.mypy] -python_version = "3.11" -warn_unused_configs = true -ignore_missing_imports = true -show_error_codes = true -pretty = true -warn_redundant_casts = true -warn_unused_ignores = true - [tool.codespell] skip = "*.html,*.css,*.js,*.svg,*.lock,.git,__pycache__,*.egg-info,build,dist,docs/_build,*.map" -ignore-words-list = "nd,ot,fro" +ignore-words-list = "nd,ot,fro,sems,pre-select,Disjointness" [tool.towncrier] package = "mne_denoise" @@ -221,6 +206,13 @@ issue_format = "[#{issue}](https://github.com/mne-tools/mne-denoise/issues/{issu template = "docs/changes/template.jinja" underlines = ["", "", ""] +[tool.changelog-bot] + +[tool.changelog-bot.towncrier_changelog] +enabled = true +verify_pr_number = true +changelog_skip_label = "no-changelog-entry-needed" + [[tool.towncrier.type]] directory = "feature" name = "Added" @@ -245,3 +237,15 @@ showcontent = true directory = "misc" name = "Internal" showcontent = true + +[tool.spin] +package = "mne_denoise" + +[tool.spin.commands] +"Development" = [ + ".spin/cmds.py:test", + ".spin/cmds.py:lint", + ".spin/cmds.py:docs", + ".spin/cmds.py:build", + ".spin/cmds.py:check", +] diff --git a/scripts/check_dist.py b/scripts/check_dist.py index e89aaf8f..5e5c34a6 100644 --- a/scripts/check_dist.py +++ b/scripts/check_dist.py @@ -19,12 +19,16 @@ "viz": "matplotlib", } DEVELOPMENT_REQUIREMENTS = { + "build", "pytest", "sphinx", "ruff", - "pre-commit", - "mypy", - "build", + "prek", + "spin", + "yamllint", + "zizmor", + "codespell", + "pre-commit-hooks", "twine", "towncrier", } @@ -84,7 +88,7 @@ def _check_wheel(wheel: Path) -> Version: _, metadata = _wheel_metadata(archive) assert metadata["Name"] == "mne-denoise" - assert metadata["Requires-Python"] == ">=3.11" + assert metadata["Requires-Python"] == ">=3.12" assert metadata["License-Expression"] == "BSD-3-Clause" assert "LICENSE" in metadata.get_all("License-File", []) diff --git a/scripts/rename_towncrier.py b/scripts/rename_towncrier.py new file mode 100644 index 00000000..f4d0eadd --- /dev/null +++ b/scripts/rename_towncrier.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Rename an unnumbered Towncrier fragment to the current pull request.""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import tomllib +from pathlib import Path +from typing import Any + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--pr-number", + type=int, + help="Pull request number; otherwise derive it from GITHUB_EVENT_PATH.", + ) + return parser + + +def _event_pr_number() -> int | None: + """Return the pull request number from a GitHub event, if applicable.""" + event_name = os.environ.get("GITHUB_EVENT_NAME", "") + event_path = os.environ.get("GITHUB_EVENT_PATH") + if not event_name.startswith("pull_request"): + print(f"No-op: {event_name or 'local execution'} is not a pull request.") + return None + if not event_path: + raise RuntimeError("GITHUB_EVENT_PATH is required for pull request events") + with Path(event_path).open(encoding="utf-8") as file: + event: dict[str, Any] = json.load(file) + number = event.get("number") or event.get("pull_request", {}).get("number") + if not isinstance(number, int) or number <= 0: + raise RuntimeError("could not find a positive pull request number in the event") + return number + + +def _towncrier_config() -> tuple[Path, tuple[str, ...]]: + """Return the configured fragment directory and supported fragment types.""" + with Path("pyproject.toml").open("rb") as file: + config = tomllib.load(file) + towncrier = config["tool"]["towncrier"] + directory = Path(towncrier["directory"]) + types = tuple(entry["directory"] for entry in towncrier["type"]) + if directory.is_absolute() or not types: + raise RuntimeError("Towncrier directory and types must be configured locally") + return directory, types + + +def rename(pr_number: int) -> int: + """Rename all supported unnumbered fragments and return a process status.""" + directory, types = _towncrier_config() + operations = [ + ( + directory / f"{fragment_type}.rst", + directory / f"{pr_number}.{fragment_type}.rst", + ) + for fragment_type in types + if (directory / f"{fragment_type}.rst").is_file() + ] + if not operations: + print(f"No unnumbered Towncrier fragments found in {directory}.") + return 0 + collisions = [target for _, target in operations if target.exists()] + if collisions: + names = ", ".join(str(path) for path in collisions) + print( + f"Refusing to overwrite existing Towncrier fragment(s): {names}", + file=sys.stderr, + ) + return 1 + for source, target in operations: + source.rename(target) + print(f"Renamed {source} -> {target}") + return 0 + + +def main() -> int: + """Run the Towncrier fragment renamer.""" + args = _parser().parse_args() + try: + pr_number = args.pr_number if args.pr_number is not None else _event_pr_number() + return 0 if pr_number is None else rename(pr_number) + except (OSError, KeyError, TypeError, ValueError, RuntimeError) as error: + print(f"Towncrier fragment renaming failed: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/pylock.ci-old.toml b/tools/pylock.ci-old.toml index af299ca7..c8d41a07 100644 --- a/tools/pylock.ci-old.toml +++ b/tools/pylock.ci-old.toml @@ -1,8 +1,8 @@ # This file was autogenerated by uv via the following command: -# uv pip compile pyproject.toml --python 3.11 --python-platform x86_64-unknown-linux-gnu --group test --group lockfile_extras --resolution lowest-direct --format pylock.toml --output-file tools/pylock.ci-old.toml --no-cache +# uv pip compile pyproject.toml --python 3.12 --python-platform x86_64-unknown-linux-gnu --group test --group lockfile_extras --resolution lowest-direct --format pylock.toml --output-file tools/pylock.ci-old.toml --no-cache lock-version = "1.0" created-by = "uv" -requires-python = ">=3.11" +requires-python = ">=3.12" [[packages]] name = "certifi" @@ -15,7 +15,7 @@ name = "charset-normalizer" version = "3.5.1" sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", upload-time = 2026-08-15T08:20:44Z, size = 171764, hashes = { sha256 = "6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3" } } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/35/731ac04aa0a097fc1c97f0994c375bdb230c6c96619db794208fe664e9ce/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-08-15T08:16:54Z, size = 262325, hashes = { sha256 = "c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8" } }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-08-15T08:17:17Z, size = 248801, hashes = { sha256 = "b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08" } }, { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", upload-time = 2026-08-15T08:19:52Z, size = 253057, hashes = { sha256 = "a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99" } }, { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", upload-time = 2026-08-15T08:20:43Z, size = 68658, hashes = { sha256 = "6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6" } }, ] @@ -24,14 +24,14 @@ wheels = [ name = "contourpy" version = "1.3.3" sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", upload-time = 2025-07-26T12:03:12Z, size = 13466174, hashes = { sha256 = "083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2025-07-26T12:01:10Z, size = 355238, hashes = { sha256 = "51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db" } }] +wheels = [{ url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2025-07-26T12:01:28Z, size = 362601, hashes = { sha256 = "4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1" } }] [[packages]] name = "coverage" version = "7.16.0" sdist = { url = "https://files.pythonhosted.org/packages/d1/f5/deb1a27aa20746c0278ac998c4179e272004699b2d33959ce020c5ac1615/coverage-7.16.0.tar.gz", upload-time = 2026-08-28T21:54:37Z, size = 945620, hashes = { sha256 = "077f0964087883176ff6ab9b074694cae29f8c708273b13ca62c183c6ed716cd" } } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/27/ade10badacc00076854f0c5086fcf8975bb1a379d5288b587509e6ee9763/coverage-7.16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", upload-time = 2026-08-28T21:51:06Z, size = 255846, hashes = { sha256 = "7cae7715afa51dd7c9c42e6603bb46daf424c3449fdf06519cc658aa8d46e2e4" } }, + { url = "https://files.pythonhosted.org/packages/95/29/dd89fd39af1a3b6e9a9c3eddeaf03f6376ba517d43d6cbf8b519177e2a10/coverage-7.16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", upload-time = 2026-08-28T21:51:33Z, size = 257790, hashes = { sha256 = "719a3feb6220dd32ed932d4c3676d17fb8739e2643b29c0e7c3af400ff80ac44" } }, { url = "https://files.pythonhosted.org/packages/b1/5a/234e8fadf85c3cc48cb31c247b9e8e0c7f06ece80f5b29f9b8c241f9da4c/coverage-7.16.0-py3-none-any.whl", upload-time = 2026-08-28T21:54:35Z, size = 214977, hashes = { sha256 = "245f7de6d023a5bba375dbec9f2e0869bfa26ac0cc639bbb7b4c814884000b73" } }, ] @@ -52,7 +52,7 @@ name = "fonttools" version = "4.63.0" sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", upload-time = 2026-05-14T12:04:30Z, size = 3597189, hashes = { sha256 = "caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0" } } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/43/a81f20050a3115b57d62c8e781446949512eac36690dc384ccea65ff4cc1/fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-05-14T12:03:03Z, size = 5082308, hashes = { sha256 = "d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18" } }, + { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-05-14T12:03:20Z, size = 4999800, hashes = { sha256 = "58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8" } }, { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", upload-time = 2026-05-14T12:04:29Z, size = 1164562, hashes = { sha256 = "445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d" } }, ] @@ -84,7 +84,7 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/91/d4/3b4c8e5a30604df name = "kiwisolver" version = "1.5.1" sdist = { url = "https://files.pythonhosted.org/packages/ba/07/bd78e6a8fae171ea041ef5bba3ed21a003522fa088834b069b1909981f30/kiwisolver-1.5.1.tar.gz", upload-time = 2026-08-28T10:28:27Z, size = 104395, hashes = { sha256 = "f1303ef2eec81262a4b708c3e858afe58d7c75ad91c1c05266eda7673369859a" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/89/00/05c2d0369ac322d22d5c05f84b5c4a6856fa6207fbae42869108a28f0383/kiwisolver-1.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-08-28T10:25:08Z, size = 1438206, hashes = { sha256 = "95a02752aa032eef4aed01cda6d9b687c669bd0396bf4519eef8bba22a286720" } }] +wheels = [{ url = "https://files.pythonhosted.org/packages/fc/f4/dadfec469313c7f428efa7e84b4aba9732f813c13ea7131a24b7b008ef57/kiwisolver-1.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-08-28T10:25:31Z, size = 1477929, hashes = { sha256 = "34633ecf50d16187ab8e5528b7a2530f2feb4e23f300db4672538b51cfc5cd38" } }] [[packages]] name = "lazy-loader" @@ -96,13 +96,13 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da168 name = "markupsafe" version = "3.0.3" sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", upload-time = 2025-09-27T18:37:40Z, size = 80313, hashes = { sha256 = "722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2025-09-27T18:36:22Z, size = 22940, hashes = { sha256 = "0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf" } }] +wheels = [{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2025-09-27T18:36:33Z, size = 22947, hashes = { sha256 = "d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d" } }] [[packages]] name = "matplotlib" version = "3.8.0" sdist = { url = "https://files.pythonhosted.org/packages/23/e1/77016194621fb1356aafeb2186f07b5dede62ea2043bf03f82325c4fccc5/matplotlib-3.8.0.tar.gz", upload-time = 2023-09-15T04:49:03Z, size = 35864435, hashes = { sha256 = "df8505e1c19d5c2c26aff3497a7cbd3ccfc2e97043d1e4db3e76afa399164b69" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/65/5b/3b8fd7d66043f0638a35fa650570cbe69efd42fe169e5024f9307598b47e/matplotlib-3.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2023-09-15T04:51:00Z, size = 11615276, hashes = { sha256 = "eee482731c8c17d86d9ddb5194d38621f9b0f0d53c99006275a12523ab021732" } }] +wheels = [{ url = "https://files.pythonhosted.org/packages/77/cd/1464efc9fe354026b8d2fb4ebb2f1746559b0e38308104d3ee60a5a05c71/matplotlib-3.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2023-09-15T04:51:54Z, size = 11602343, hashes = { sha256 = "dae97fdd6996b3a25da8ee43e3fc734fff502f396801063c6b76c20b56683196" } }] [[packages]] name = "mne" @@ -114,7 +114,7 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/6c/da/a3280dbd8f0024b name = "numpy" version = "1.26.0" sdist = { url = "https://files.pythonhosted.org/packages/55/b3/b13bce39ba82b7398c06d10446f5ffd5c07db39b09bd37370dc720c7951c/numpy-1.26.0.tar.gz", upload-time = 2023-09-16T20:12:58Z, size = 15633455, hashes = { sha256 = "f93fc78fe8bf15afe2b8d6b6499f1c73953169fad1e9a8dd086cdff3190e7fdf" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/c4/36/161e2f8110f8c49e59f6107bd6da4257d30aff9f06373d0471811f73dcc5/numpy-1.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2023-09-16T20:02:49Z, size = 18178118, hashes = { sha256 = "e062aa24638bb5018b7841977c360d2f5917268d125c833a686b7cbabbec496c" } }] +wheels = [{ url = "https://files.pythonhosted.org/packages/e3/e2/4ecfbc4a2e3f9d227b008c92a5d1f0370190a639b24fec3b226841eaaf19/numpy-1.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2023-09-16T20:05:55Z, size = 17883864, hashes = { sha256 = "7f6bad22a791226d0a5c7c27a80a20e11cfe09ad5ef9084d4d3fc4a299cca505" } }] [[packages]] name = "packaging" @@ -124,15 +124,15 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9ead [[packages]] name = "pandas" -version = "1.5.0" -sdist = { url = "https://files.pythonhosted.org/packages/2a/24/f5042daa59b91e94e6ea41edbb28d2b7e3712d0cf54a76f9ffde394efbe7/pandas-1.5.0.tar.gz", upload-time = 2022-09-19T15:55:13Z, size = 5191537, hashes = { sha256 = "3ee61b881d2f64dd90c356eb4a4a4de75376586cd3c9341c6c0fcaae18d52977" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/fa/fe/c81ad3991f2c6aeacf01973f1d37b1dc76c0682f312f104741602a9557f1/pandas-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2022-09-19T15:53:58Z, size = 12022642, hashes = { sha256 = "e252a9e49b233ff96e2815c67c29702ac3a062098d80a170c506dff3470fd060" } }] +version = "2.1.1" +sdist = { url = "https://files.pythonhosted.org/packages/3d/0e/2c225d7a5de6ca0ec7d729aff6ef560544596f3a9bfed77f6dbc1713dbb5/pandas-2.1.1.tar.gz", upload-time = 2023-09-20T21:05:19Z, size = 4266250, hashes = { sha256 = "fecb198dc389429be557cde50a2d46da8434a17fe37d7d41ff102e3987fd947b" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/41/db/fc107df31c06976764e753074cc71cbe1c7062481f668746f8d498cafcb6/pandas-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2023-09-20T21:04:48Z, size = 11652903, hashes = { sha256 = "29deb61de5a8a93bdd033df328441a79fcf8dd3c12d5ed0b41a395eef9cd76f0" } }] [[packages]] name = "pillow" version = "12.3.0" sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", upload-time = 2026-07-01T11:56:38Z, size = 47025035, hashes = { sha256 = "3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-07-01T11:53:53Z, size = 6934408, hashes = { sha256 = "23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd" } }] +wheels = [{ url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-07-01T11:54:13Z, size = 6940830, hashes = { sha256 = "78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91" } }] [[packages]] name = "platformdirs" @@ -198,13 +198,13 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e name = "scikit-learn" version = "1.5.0" sdist = { url = "https://files.pythonhosted.org/packages/bf/8a/06e499bca463905000f50e461c9445e949aafdd33ea3b62024aa2238b83d/scikit_learn-1.5.0.tar.gz", upload-time = 2024-05-21T16:34:07Z, size = 7820839, hashes = { sha256 = "789e3db01c750ed6d496fa2db7d50637857b451e57bcae863bff707c1247bef7" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/46/c0/63d3a8da39a2ee051df229111aa93f6dca2b56f8080abd34993938166455/scikit_learn-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-05-21T16:33:29Z, size = 13328661, hashes = { sha256 = "118a8d229a41158c9f90093e46b3737120a165181a1b58c03461447aa4657415" } }] +wheels = [{ url = "https://files.pythonhosted.org/packages/ae/54/e70102a9c12d27d985ba659f336851732415e5a02864bef2ead36afaf15d/scikit_learn-1.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-05-21T16:33:45Z, size = 13065320, hashes = { sha256 = "a3a10e1d9e834e84d05e468ec501a356226338778769317ee0b84043c0d8fb06" } }] [[packages]] name = "scipy" version = "1.13.0" sdist = { url = "https://files.pythonhosted.org/packages/fb/a3/328965862f41ba67d27ddd26205962007ec87d99eec6d364a29bf00ac093/scipy-1.13.0.tar.gz", upload-time = 2024-04-02T21:48:22Z, size = 57204550, hashes = { sha256 = "58569af537ea29d3f78e5abd18398459f195546bb3be23d16677fb26616cc11e" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/e8/fb/e5955e2ddbdf2baee461eb53ec8d0adedd20a6dfc5510ef8d5e7e44ba461/scipy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-04-02T21:42:22Z, size = 38562576, hashes = { sha256 = "9ff7dad5d24a8045d836671e082a490848e8639cabb3dbdacb29f943a678683d" } }] +wheels = [{ url = "https://files.pythonhosted.org/packages/87/8c/97e545034c94d0bbbc3af3202551c3d6020e5f8d2ee37ebcabd9a2048174/scipy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-04-02T21:43:02Z, size = 38213102, hashes = { sha256 = "1e7626dfd91cdea5714f343ce1176b6c4745155d234f1033584154f60ef1ff42" } }] [[packages]] name = "seaborn" @@ -224,22 +224,18 @@ version = "3.6.0" sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", upload-time = 2025-03-13T13:49:23Z, size = 21274, hashes = { sha256 = "8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e" } } wheels = [{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", upload-time = 2025-03-13T13:49:21Z, size = 18638, hashes = { sha256 = "43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb" } }] -[[packages]] -name = "tomli" -version = "2.4.1" -marker = "python_full_version <= '3.11'" -sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", upload-time = 2026-03-25T20:22:03Z, size = 17543, hashes = { sha256 = "7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f" } } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-03-25T20:21:14Z, size = 243824, hashes = { sha256 = "5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9" } }, - { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", upload-time = 2026-03-25T20:22:03Z, size = 14583, hashes = { sha256 = "0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe" } }, -] - [[packages]] name = "tqdm" version = "4.66.0" sdist = { url = "https://files.pythonhosted.org/packages/f1/fb/6f40278d3b74f1486147aebf828ad081226f4f80b5b31a042386acc76dde/tqdm-4.66.0.tar.gz", upload-time = 2023-08-09T10:45:13Z, size = 169029, hashes = { sha256 = "cc6e7e52202d894e66632c5c8a9330bd0e3ff35d2965c93ca832114a3d865362" } } wheels = [{ url = "https://files.pythonhosted.org/packages/a5/d6/502a859bac4ad5e274255576cd3e15ca273cdb91731bc39fb840dd422ee9/tqdm-4.66.0-py3-none-any.whl", upload-time = 2023-08-09T10:45:10Z, size = 78149, hashes = { sha256 = "39d459c7140b7890174e69d4d68d6291bc774a55b4bc5d93c0b760798ac5a03e" } }] +[[packages]] +name = "tzdata" +version = "2026.3" +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", upload-time = 2026-07-10T08:50:37Z, size = 198674, hashes = { sha256 = "4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", upload-time = 2026-07-10T08:50:36Z, size = 348168, hashes = { sha256 = "dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931" } }] + [[packages]] name = "urllib3" version = "2.7.0" From b9a2517829e91a0c9e8b78a410cb9e6848c036aa Mon Sep 17 00:00:00 2001 From: Hamza Abdelhedi Date: Sun, 30 Aug 2026 16:43:33 -0400 Subject: [PATCH 02/11] CI automate repository maintenance --- .github/dependabot.yml | 19 ++++-- .github/workflows/autofix.yml | 87 +++++++++++++++++++++++++ .github/workflows/check_changelog.yml | 27 ++++++++ .github/workflows/dependency-review.yml | 21 ++++++ .github/workflows/docs.yml | 24 +++---- .github/workflows/release.yml | 12 ++-- .github/workflows/tests.yml | 72 ++++++++++---------- .spin/cmds.py | 6 +- tests/test_rename_towncrier.py | 78 ++++++++++++++++++++++ 9 files changed, 284 insertions(+), 62 deletions(-) create mode 100644 .github/workflows/autofix.yml create mode 100644 .github/workflows/check_changelog.yml create mode 100644 .github/workflows/dependency-review.yml create mode 100644 tests/test_rename_towncrier.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4079c986..6849bfef 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,13 +1,20 @@ version: 2 updates: - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 10 - - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" + day: "monday" + time: "05:17" + timezone: "UTC" + cooldown: + default-days: 7 + commit-message: + prefix: "[dependabot]" + groups: + github-actions: + patterns: + - "*" + labels: + - "no-changelog-entry-needed" open-pull-requests-limit: 10 diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 00000000..54b67ab2 --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,87 @@ +name: autofix.ci + +on: # yamllint disable-line rule:truthy + pull_request: + branches: [main] + types: [opened, synchronize, labeled, unlabeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: read + +jobs: + autofix: + name: Apply repository fixes + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + cache: pip + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + - name: Checkout shared MNE-tools checker + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: mne-tools/mne-tools + ref: 2e7c4b5469ef311a169e16c7f97cd0ce68508a61 + path: .ci/mne-tools + fetch-depth: 1 + persist-credentials: false + - name: Install shared lockfile checker + run: python -m pip install -e "$GITHUB_WORKSPACE/.ci/mne-tools" + - name: Install maintenance tools + run: | + python -m pip install --upgrade pip + python -m pip install --group lint --group changelog + - name: Number the changelog fragment + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: python scripts/rename_towncrier.py --pr-number "$PR_NUMBER" + - name: Detect dependency changes + id: changed + env: + GH_REPO: ${{ github.repository }} + GH_PR_NUMBER: ${{ github.event.pull_request.number }} + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + changed_files="$(gh api --paginate \ + --header 'Accept: application/vnd.github+json' \ + "repos/${GH_REPO}/pulls/${GH_PR_NUMBER}/files?per_page=100" \ + --jq '.[].filename')" + if grep -Eq '^(pyproject\.toml|tools/pylock\.ci-old\.toml|\.github/workflows/autofix\.yml)$' <<<"$changed_files"; then + echo "dependencies=true" >> "$GITHUB_OUTPUT" + else + echo "dependencies=false" >> "$GITHUB_OUTPUT" + fi + - name: Regenerate lower-bound lockfile + if: steps.changed.outputs.dependencies == 'true' + run: >- + uv pip compile pyproject.toml --python "3.12" + --python-platform "x86_64-unknown-linux-gnu" --group test + --group lockfile_extras --resolution lowest-direct --format pylock.toml + --output-file tools/pylock.ci-old.toml --no-cache + - name: Validate lower-bound lockfile + if: steps.changed.outputs.dependencies == 'true' + run: | + python -m mne_tools.check_lockfile \ + "$GITHUB_WORKSPACE" \ + tools/pylock.ci-old.toml \ + --groups lockfile_extras + - name: Run repository hooks + run: prek run --all-files + - name: Apply fixes through autofix.ci + if: success() || failure() + uses: autofix-ci/action@c5b2d67aa2274e7b5a18224e8171550871fc7e4a # v1.3.4 diff --git a/.github/workflows/check_changelog.yml b/.github/workflows/check_changelog.yml new file mode 100644 index 00000000..2e6ccb47 --- /dev/null +++ b/.github/workflows/check_changelog.yml @@ -0,0 +1,27 @@ +name: Changelog + +on: + pull_request: + branches: [main] + types: [opened, synchronize, labeled, unlabeled] + +permissions: + contents: read + pull-requests: read + +jobs: + changelog: + name: Check changelog entry + runs-on: ubuntu-latest + timeout-minutes: 10 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: scientific-python/action-towncrier-changelog@165df35cfb3ff4b5bfea7645c4b923c6e8c7b54a # v2.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BOT_USERNAME: changelog-bot diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000..f80c5ab2 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,21 @@ +name: Dependency review + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + dependency-review: + name: Dependency review + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-severity: high diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3551682b..13dcaab4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,11 +27,11 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@v7 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" cache: pip @@ -52,7 +52,7 @@ jobs: run: python -m pip check - name: Restore documentation datasets id: docs-data-cache - uses: actions/cache/restore@v6 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.MNE_DATA }} key: docs-data-${{ runner.os }}-${{ hashFiles('scripts/prefetch_docs_data.py', 'pyproject.toml') }} @@ -66,7 +66,7 @@ jobs: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') - uses: actions/cache/save@v6 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.MNE_DATA }} key: docs-data-${{ runner.os }}-${{ hashFiles('scripts/prefetch_docs_data.py', 'pyproject.toml') }} @@ -83,7 +83,7 @@ jobs: test -s docs/_build/html/auto_examples/zapline/plot_03_epoched_data.html echo "$(find docs/_build/html -name '*.html' | wc -l) HTML pages built" - name: Upload documentation artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: docs-html-stable-mne path: docs/_build/html/ @@ -99,11 +99,11 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@v7 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" cache: pip @@ -112,7 +112,7 @@ jobs: python -m pip install --upgrade pip python -m pip install -e . --group doc python -c "import mne; print(mne.__version__)" > "$RUNNER_TEMP/docs-mne-stable-version" - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: mne-tools/mne-python path: .ci/mne-python @@ -142,7 +142,7 @@ jobs: raise SystemExit("MNE main installation did not replace stable MNE") PY - name: Restore documentation datasets - uses: actions/cache/restore@v6 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.MNE_DATA }} key: docs-data-${{ runner.os }}-${{ hashFiles('scripts/prefetch_docs_data.py', 'pyproject.toml') }} @@ -165,7 +165,7 @@ jobs: test -s docs/_build/html/auto_examples/zapline/plot_03_epoched_data.html echo "$(find docs/_build/html -name '*.html' | wc -l) HTML pages built" - name: Upload documentation artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: docs-html-mne-main path: docs/_build/html/ @@ -181,12 +181,12 @@ jobs: contents: write steps: - name: Download stable-MNE documentation - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: docs-html-stable-mne path: docs/_build/html/ - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v4 + uses: peaceiris/actions-gh-pages@1ef5a1b1df4c63fe21a2242edbee6cac921ece01 # v4.1.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: docs/_build/html/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7bf86c1b..d97b54e5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,13 +20,13 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@v7 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "3.11" + python-version: "3.12" - name: Install build dependencies run: | python -m pip install --upgrade pip @@ -108,7 +108,7 @@ jobs: "$wheel_test/bin/python" "$GITHUB_WORKSPACE/scripts/check_base_install.py" "$wheel_test/bin/python" -m pip check - name: Upload release distributions - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: release-dists path: dist/ @@ -127,11 +127,11 @@ jobs: name: pypi url: https://pypi.org/p/mne-denoise steps: - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: release-dists path: dist/ - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@4bb033805d9e19112d8c697528791ff53f6c2f74 # v1.9.0 with: print-hash: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a82157ad..6f8e96c4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,6 @@ name: Tests -on: +on: # yamllint disable-line rule:truthy push: branches: [main] pull_request: @@ -26,11 +26,11 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@v7 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" cache: pip @@ -38,15 +38,11 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install --group lint - - name: Run ruff check - run: ruff check . - - name: Run ruff format check - run: ruff format --check . - - name: Run pre-commit - run: pre-commit run --all-files + - name: Run repository checks + run: spin lint base-install: - name: Base install / Python 3.11 + name: Base install / Python 3.12 if: github.event_name != 'schedule' runs-on: ubuntu-latest timeout-minutes: 10 @@ -54,13 +50,13 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@v7 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "3.11" + python-version: "3.12" - name: Validate base installation run: | base_python="$RUNNER_TEMP/mne-denoise-base/bin/python" @@ -89,28 +85,38 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: astral-sh/setup-uv@v10.0.1 + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: version: ">=0.9" - python-version: "3.11" + python-version: "3.12" activate-environment: true cache-dependency-glob: | pyproject.toml tools/pylock.ci-old.toml - - name: Validate lower-bound lockfile - uses: mne-tools/mne-tools/actions/check-lockfile@main + - name: Checkout shared MNE-tools checker + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - project-root: ${{ github.workspace }} - lockfile-path: tools/pylock.ci-old.toml - groups: lockfile_extras + repository: mne-tools/mne-tools + ref: 2e7c4b5469ef311a169e16c7f97cd0ce68508a61 + path: .ci/mne-tools + fetch-depth: 1 + persist-credentials: false + - name: Install shared lockfile checker + run: python -m pip install -e "$GITHUB_WORKSPACE/.ci/mne-tools" + - name: Validate lower-bound lockfile + run: | + python -m mne_tools.check_lockfile \ + "$GITHUB_WORKSPACE" \ + tools/pylock.ci-old.toml \ + --groups lockfile_extras - name: Restore lower-bound Python environment run: | minimum_env="$RUNNER_TEMP/mne-denoise-minimum" - uv venv --python 3.11 "$minimum_env" + uv venv --python 3.12 "$minimum_env" echo "VIRTUAL_ENV=$minimum_env" >> "$GITHUB_ENV" echo "$minimum_env/bin" >> "$GITHUB_PATH" - name: Install lower-bound environment @@ -120,7 +126,7 @@ jobs: uv pip install --python "$minimum_python" pip uv pip install --python "$minimum_python" -e . --no-deps - name: Check declared lower bounds - uses: mne-tools/mne-tools/actions/check-environment@main + uses: mne-tools/mne-tools/actions/check-environment@2e7c4b5469ef311a169e16c7f97cd0ce68508a61 # audited mne-tools main with: project-root: ${{ github.workspace }} groups: lockfile_extras @@ -156,10 +162,6 @@ jobs: fail-fast: false matrix: include: - - os: ubuntu-latest - python-version: "3.12" - name: Python 3.12 - coverage: "false" - os: ubuntu-latest python-version: "3.13" name: Python 3.13 @@ -180,11 +182,11 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@v7 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} cache: pip @@ -231,7 +233,7 @@ jobs: run: pytest --cov=mne_denoise --cov-report=xml -q - name: Upload coverage to Codecov if: matrix.coverage == 'true' - uses: codecov/codecov-action@v7 + uses: codecov/codecov-action@a99c28d3f0da835de33ff2feb2e15691c7b9641f # v7.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml @@ -246,11 +248,11 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@v7 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" cache: pip @@ -259,7 +261,7 @@ jobs: python -m pip install --upgrade pip python -m pip install -e . --group test python -c "import mne; print(mne.__version__)" > "$RUNNER_TEMP/mne-stable-version" - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: mne-tools/mne-python path: .ci/mne-python @@ -319,11 +321,11 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@v7 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" cache: pip diff --git a/.spin/cmds.py b/.spin/cmds.py index 974a1ae0..26f12d36 100644 --- a/.spin/cmds.py +++ b/.spin/cmds.py @@ -83,6 +83,6 @@ def build() -> None: @click.command() def check() -> None: """Run hooks, tests, and distribution validation.""" - _run("spin", "lint") - _run("spin", "test") - _run("spin", "build") + _run(sys.executable, "-m", "spin", "lint") + _run(sys.executable, "-m", "spin", "test") + _run(sys.executable, "-m", "spin", "build") diff --git a/tests/test_rename_towncrier.py b/tests/test_rename_towncrier.py new file mode 100644 index 00000000..df5b10b3 --- /dev/null +++ b/tests/test_rename_towncrier.py @@ -0,0 +1,78 @@ +"""Tests for the local Towncrier fragment helper.""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +SCRIPT = Path(__file__).parents[1] / "scripts" / "rename_towncrier.py" +CONFIG = """ +[tool.towncrier] +directory = "docs/changes/devel/" + +[[tool.towncrier.type]] +directory = "feature" + +[[tool.towncrier.type]] +directory = "bugfix" + +[[tool.towncrier.type]] +directory = "doc" + +[[tool.towncrier.type]] +directory = "removal" + +[[tool.towncrier.type]] +directory = "misc" +""" + + +def _prepare(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text(CONFIG, encoding="utf-8") + (tmp_path / "docs" / "changes" / "devel").mkdir(parents=True, exist_ok=True) + + +def _run(tmp_path: Path, *args: str) -> subprocess.CompletedProcess[str]: + _prepare(tmp_path) + return subprocess.run( + [sys.executable, str(SCRIPT), *args], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + + +def test_rename_misc_fragment(tmp_path: Path) -> None: + _prepare(tmp_path) + fragment = tmp_path / "docs" / "changes" / "devel" / "misc.rst" + fragment.write_text("Tooling maintenance.\n", encoding="utf-8") + + result = _run(tmp_path, "--pr-number", "123") + + assert result.returncode == 0 + assert not fragment.exists() + assert (fragment.parent / "123.misc.rst").read_text(encoding="utf-8") == ( + "Tooling maintenance.\n" + ) + + +def test_rename_succeeds_without_fragments(tmp_path: Path) -> None: + result = _run(tmp_path, "--pr-number", "123") + + assert result.returncode == 0 + assert "No unnumbered" in result.stdout + + +def test_rename_refuses_to_overwrite(tmp_path: Path) -> None: + _prepare(tmp_path) + fragment_dir = tmp_path / "docs" / "changes" / "devel" + (fragment_dir / "misc.rst").write_text("new\n", encoding="utf-8") + (fragment_dir / "123.misc.rst").write_text("old\n", encoding="utf-8") + + result = _run(tmp_path, "--pr-number", "123") + + assert result.returncode == 1 + assert "Refusing to overwrite" in result.stderr + assert (fragment_dir / "misc.rst").exists() From 112c63b99c8c49b82e918b93fc99dfe35fc0d56a Mon Sep 17 00:00:00 2001 From: Hamza Abdelhedi Date: Sun, 30 Aug 2026 16:46:18 -0400 Subject: [PATCH 03/11] CI harden workflow supply chain --- .../rename_towncrier/rename_towncrier.py | 59 ------------------- .github/workflows/check_changelog.yml | 6 +- .github/workflows/dependency-review.yml | 6 +- .github/workflows/docs.yml | 26 ++++---- .github/workflows/release.yml | 12 ++-- .github/workflows/tests.yml | 2 +- 6 files changed, 26 insertions(+), 85 deletions(-) delete mode 100644 .github/actions/rename_towncrier/rename_towncrier.py diff --git a/.github/actions/rename_towncrier/rename_towncrier.py b/.github/actions/rename_towncrier/rename_towncrier.py deleted file mode 100644 index 89be8990..00000000 --- a/.github/actions/rename_towncrier/rename_towncrier.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 - -# Authors: The MNE-Python contributors. -# License: BSD-3-Clause -# Copyright the MNE-Python contributors. -# Copied from mne-python: -# https://github.com/mne-tools/mne-python/blob/main/.github/actions/rename_towncrier/rename_towncrier.py - -import json -import os -import re -import subprocess -import sys -from pathlib import Path -from tomllib import loads - -from github import Github - -event_name = os.getenv("GITHUB_EVENT_NAME", "pull_request") -if not event_name.startswith("pull_request"): - print(f"No-op for {event_name}") - sys.exit(0) -if "GITHUB_EVENT_PATH" in os.environ: - with open(os.environ["GITHUB_EVENT_PATH"], encoding="utf-8") as fin: - event = json.load(fin) - pr_num = event["number"] - basereponame = event["pull_request"]["base"]["repo"]["full_name"] - real = True -else: # local testing - pr_num = 12318 # added some towncrier files - basereponame = "mne-tools/mne-python" - real = False - -g = Github(os.environ.get("GITHUB_TOKEN")) -baserepo = g.get_repo(basereponame) - -# Grab config from upstream's default branch -toml_cfg = loads(Path("pyproject.toml").read_text("utf-8")) - -config = toml_cfg["tool"]["towncrier"] -pr = baserepo.get_pull(pr_num) -modified_files = [f.filename for f in pr.get_files()] - -# Get types from config -types = [ent["directory"] for ent in toml_cfg["tool"]["towncrier"]["type"]] -type_pipe = "|".join(types) - -# Get files that potentially match the types -directory = toml_cfg["tool"]["towncrier"]["directory"] -assert directory.endswith("/"), directory - -file_re = re.compile(rf"^{directory}({type_pipe})\.rst$") -found_stubs = [f for f in modified_files if file_re.match(f)] -for stub in found_stubs: - fro = stub - to = file_re.sub(rf"{directory}{pr_num}.\1.rst", fro) - print(f"Renaming {fro} to {to}") - if real: - subprocess.check_call(["mv", fro, to]) diff --git a/.github/workflows/check_changelog.yml b/.github/workflows/check_changelog.yml index 2e6ccb47..c072f45f 100644 --- a/.github/workflows/check_changelog.yml +++ b/.github/workflows/check_changelog.yml @@ -1,6 +1,6 @@ name: Changelog -on: +on: # yamllint disable-line rule:truthy pull_request: branches: [main] types: [opened, synchronize, labeled, unlabeled] @@ -18,10 +18,10 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: scientific-python/action-towncrier-changelog@165df35cfb3ff4b5bfea7645c4b923c6e8c7b54a # v2.0.0 + - uses: scientific-python/action-towncrier-changelog@165df35cfb3ff4b5bfea7645c4b923c6e8c7b54a # v2.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BOT_USERNAME: changelog-bot diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index f80c5ab2..cdfef1a7 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -1,6 +1,6 @@ name: Dependency review -on: +on: # yamllint disable-line rule:truthy pull_request: branches: [main] @@ -13,9 +13,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 with: fail-on-severity: high diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 13dcaab4..78eea5c4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,6 +1,6 @@ name: Docs -on: +on: # yamllint disable-line rule:truthy push: branches: [main] pull_request: @@ -27,11 +27,11 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" cache: pip @@ -52,7 +52,7 @@ jobs: run: python -m pip check - name: Restore documentation datasets id: docs-data-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.MNE_DATA }} key: docs-data-${{ runner.os }}-${{ hashFiles('scripts/prefetch_docs_data.py', 'pyproject.toml') }} @@ -66,7 +66,7 @@ jobs: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.MNE_DATA }} key: docs-data-${{ runner.os }}-${{ hashFiles('scripts/prefetch_docs_data.py', 'pyproject.toml') }} @@ -83,7 +83,7 @@ jobs: test -s docs/_build/html/auto_examples/zapline/plot_03_epoched_data.html echo "$(find docs/_build/html -name '*.html' | wc -l) HTML pages built" - name: Upload documentation artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: docs-html-stable-mne path: docs/_build/html/ @@ -99,11 +99,11 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" cache: pip @@ -112,7 +112,7 @@ jobs: python -m pip install --upgrade pip python -m pip install -e . --group doc python -c "import mne; print(mne.__version__)" > "$RUNNER_TEMP/docs-mne-stable-version" - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: mne-tools/mne-python path: .ci/mne-python @@ -142,7 +142,7 @@ jobs: raise SystemExit("MNE main installation did not replace stable MNE") PY - name: Restore documentation datasets - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.MNE_DATA }} key: docs-data-${{ runner.os }}-${{ hashFiles('scripts/prefetch_docs_data.py', 'pyproject.toml') }} @@ -165,7 +165,7 @@ jobs: test -s docs/_build/html/auto_examples/zapline/plot_03_epoched_data.html echo "$(find docs/_build/html -name '*.html' | wc -l) HTML pages built" - name: Upload documentation artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: docs-html-mne-main path: docs/_build/html/ @@ -181,12 +181,12 @@ jobs: contents: write steps: - name: Download stable-MNE documentation - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: docs-html-stable-mne path: docs/_build/html/ - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@1ef5a1b1df4c63fe21a2242edbee6cac921ece01 # v4.1.0 + uses: peaceiris/actions-gh-pages@1ef5a1b1df4c63fe21a2242edbee6cac921ece01 # v4.1.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: docs/_build/html/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d97b54e5..c57e4620 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ name: Release -on: +on: # yamllint disable-line rule:truthy release: types: [published] push: @@ -20,11 +20,11 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: Install build dependencies @@ -108,7 +108,7 @@ jobs: "$wheel_test/bin/python" "$GITHUB_WORKSPACE/scripts/check_base_install.py" "$wheel_test/bin/python" -m pip check - name: Upload release distributions - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: release-dists path: dist/ @@ -127,11 +127,11 @@ jobs: name: pypi url: https://pypi.org/p/mne-denoise steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: release-dists path: dist/ - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@4bb033805d9e19112d8c697528791ff53f6c2f74 # v1.9.0 + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 with: print-hash: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6f8e96c4..f7923103 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,7 +26,7 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false From 0c741f3067a18464430ee24e336b88c4fffe96b0 Mon Sep 17 00:00:00 2001 From: Hamza Abdelhedi Date: Sun, 30 Aug 2026 16:46:44 -0400 Subject: [PATCH 04/11] DOC align development and security guidance --- .github/PULL_REQUEST_TEMPLATE.md | 6 ++-- CONTRIBUTING.md | 59 ++++++++++++++++++++------------ README.md | 4 +-- RELEASING.md | 3 +- SECURITY.md | 13 +++++++ docs/changes/README.md | 15 ++++++-- docs/getting-started.rst | 2 +- 7 files changed, 70 insertions(+), 32 deletions(-) create mode 100644 SECURITY.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ca744f59..af4154b1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -25,8 +25,8 @@ -- [ ] My code follows the code style of this project (`ruff check .`, `ruff format .`) +- [ ] My code follows the code style of this project (`spin lint`) - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have run the full test suite and all tests pass -- [ ] I have updated the documentation accordingly (`make -C docs html` builds without warnings) -- [ ] I have added an entry to `CHANGELOG.md` (if applicable) +- [ ] I have updated the documentation accordingly (`spin docs` builds without warnings) +- [ ] I have added a numbered Towncrier fragment in `docs/changes/devel/` (if applicable) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c73f839..5812b32d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ This project follows the [MNE-Python Code of Conduct](https://github.com/mne-too ### Prerequisites -- Python 3.11 or higher +- Python 3.12 or higher - Git - A GitHub account @@ -66,8 +66,8 @@ python -m pip install --upgrade pip # Install in editable mode with the development dependency group python -m pip install -e . --group dev -# Install pre-commit hooks -pre-commit install +# Install the repository hooks +prek install ``` ### Using conda @@ -80,9 +80,27 @@ conda activate mne-denoise # Install in editable mode python -m pip install --upgrade pip python -m pip install -e . --group dev -pre-commit install +prek install ``` +### Development commands + +The development dependency group includes `uv`-compatible project tooling, +`spin`, and `prek`. Spin is the project task interface, not an environment +manager: + +```bash +spin test # run pytest +spin test -- -k asr # forward arguments to pytest +spin lint # run every repository hook +spin docs # build docs with warnings as errors +spin build # build and validate distributions +spin check # lint, test, and validate distributions +``` + +Use `prek install` once to install the local Git hooks, and +`prek run --all-files` to run them directly. + ## Workflow ### 1. Create a Branch @@ -135,7 +153,11 @@ git rebase upstream/main We use [towncrier](https://towncrier.readthedocs.io/) to manage our changelog. This prevents merge conflicts and ensures standardized release notes. -When you create a Pull Request, please add a changelog entry file in `docs/changes/devel/`. The file name should be the change type (e.g., `feature.rst`, `bugfix.rst`). +When you create a Pull Request, add a changelog fragment in +`docs/changes/devel/`. Name it `..rst`, for example +`123.feature.rst`. During local development, an unnumbered `feature.rst` or +`bugfix.rst` can be renamed with `python scripts/rename_towncrier.py +--pr-number 123`; the pull-request automation performs the same conversion. For detailed instructions and available types, see [docs/changes/README.md](https://github.com/mne-tools/mne-denoise/blob/main/docs/changes/README.md). @@ -147,20 +169,14 @@ We use **Ruff** for linting and formatting, configured to follow PEP 8 with NumP ### Automatic Formatting -Pre-commit hooks will automatically format your code on commit. To run manually: +Repository hooks automatically format your code on commit. To run them manually: ```bash -# Check for linting errors -ruff check . - -# Auto-fix linting errors -ruff check . --fix - -# Format code -ruff format . +# Run the complete repository quality suite +spin lint -# Run all pre-commit hooks -pre-commit run --all-files +# Run hooks directly +prek run --all-files ``` ### Docstring Style @@ -291,8 +307,8 @@ Documentation is built with Sphinx and hosted on GitHub Pages. ### Building Docs Locally ```bash -# Build HTML documentation -make -C docs html +# Build HTML documentation with warnings as errors +spin docs # Populate the real-data cache before a full gallery build python scripts/prefetch_docs_data.py @@ -400,13 +416,12 @@ a documented deprecation process. Before submitting, ensure: -- [ ] Code follows the project style (`ruff check .` passes) -- [ ] Code is formatted (`ruff format .` produces no changes) +- [ ] Code follows the project style (`spin lint` passes) - [ ] All tests pass (`pytest` exits cleanly) - [ ] New code has tests with good coverage - [ ] Documentation is updated if needed -- [ ] Documentation builds cleanly (`make -C docs html`) -- [ ] CHANGELOG.md is updated for user-facing changes +- [ ] Documentation builds cleanly (`spin docs`) +- [ ] A numbered Towncrier fragment is included for user-facing changes - [ ] Commit messages are clear and descriptive ## Issue Guidelines diff --git a/README.md b/README.md index 3ac3314f..f2f16202 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # mne-denoise -[![CI](https://github.com/mne-tools/mne-denoise/actions/workflows/ci.yml/badge.svg)](https://github.com/mne-tools/mne-denoise/actions/workflows/ci.yml) +[![Tests](https://github.com/mne-tools/mne-denoise/actions/workflows/tests.yml/badge.svg)](https://github.com/mne-tools/mne-denoise/actions/workflows/tests.yml) [![codecov](https://codecov.io/gh/mne-tools/mne-denoise/branch/main/graph/badge.svg)](https://codecov.io/gh/mne-tools/mne-denoise) [![PyPI version](https://img.shields.io/pypi/v/mne-denoise.svg)](https://pypi.org/project/mne-denoise/) [![Python versions](https://img.shields.io/pypi/pyversions/mne-denoise.svg)](https://pypi.org/project/mne-denoise/) @@ -198,7 +198,7 @@ git clone https://github.com//mne-denoise.git cd mne-denoise python -m pip install --upgrade pip python -m pip install -e . --group dev -pre-commit install +prek install ``` ## References diff --git a/RELEASING.md b/RELEASING.md index 9f2cd7c7..850bb861 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -130,7 +130,8 @@ After the GitHub Release is published: 3. `Publish to PyPI` waits for the `pypi` GitHub Environment rules, if any. A required reviewer may approve the deployment. 4. The job downloads `release-dists` and invokes - `pypa/gh-action-pypi-publish@release/v1` with GitHub OIDC. It does not + `pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33` # v1.14.2 + with GitHub OIDC. It does not rebuild the package. The workflow uses no PyPI API token or stored upload credentials, and it does diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..eebf7f28 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security policy + +## Reporting a vulnerability + +Please report suspected vulnerabilities privately through the repository's +[GitHub security advisories page](https://github.com/mne-tools/mne-denoise/security/advisories/new) +rather than opening a public issue. Include the affected version, a minimal +reproduction, and any relevant impact or mitigation details. + +We will acknowledge reports as soon as practical and coordinate a fix and +disclosure timeline with the reporter. Security fixes are provided in the +latest supported release; users should upgrade promptly when a security +advisory is published. diff --git a/docs/changes/README.md b/docs/changes/README.md index 747724a9..5d284058 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -6,9 +6,18 @@ We use `towncrier` to manage our changelog. This ensures that changes are docume When you make a change (feature, bugfix, documentation update), you should add a fragment file to the `docs/changes/devel/` directory. -The filename should be the type of change and the extension `.rst`. The PR number will be added automatically. +The filename should include the pull request number and the change type: +`..rst`, such as `123.bugfix.rst`. For a local draft, create an +unnumbered stub such as `bugfix.rst` and run: -Format: `.rst` +```bash +python scripts/rename_towncrier.py --pr-number 123 +``` + +The pull-request automation performs this rename when needed. Do not edit +`CHANGELOG.md` in a pull request. + +Format: `..rst` ### Available types: @@ -20,7 +29,7 @@ Format: `.rst` ## Example -If you fixed a bug in a PR, create a file `docs/changes/devel/bugfix.rst`: +If you fixed a bug in PR 123, create a file `docs/changes/devel/123.bugfix.rst`: ```rst Fixed a bug where the ZapLine algorithm would crash on empty data. diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 5e384d1a..4e14f3fa 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -24,7 +24,7 @@ dependency group: python -m pip install --upgrade pip python -m pip install -e . --group dev - pre-commit install + prek install Basic usage ----------- From 155655396b86b28d49b42bbcb83bb8d00452ba84 Mon Sep 17 00:00:00 2001 From: Hamza Abdelhedi Date: Sun, 30 Aug 2026 17:35:24 -0400 Subject: [PATCH 05/11] DOC add PR changelog fragments --- docs/changes/devel/99.misc.rst | 3 +++ docs/changes/devel/99.removal.rst | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 docs/changes/devel/99.misc.rst create mode 100644 docs/changes/devel/99.removal.rst diff --git a/docs/changes/devel/99.misc.rst b/docs/changes/devel/99.misc.rst new file mode 100644 index 00000000..3d64556d --- /dev/null +++ b/docs/changes/devel/99.misc.rst @@ -0,0 +1,3 @@ +Developer tooling and CI maintenance now use Spin, prek, immutable GitHub +Actions, automated lower-bound dependency validation, dependency review, and +security-focused repository checks. diff --git a/docs/changes/devel/99.removal.rst b/docs/changes/devel/99.removal.rst new file mode 100644 index 00000000..dddc5d84 --- /dev/null +++ b/docs/changes/devel/99.removal.rst @@ -0,0 +1,2 @@ +Python 3.11 support has been removed. Python 3.12 is now the minimum +supported Python version. From 5dc30c33457e555394e453a6daaadde3906194d9 Mon Sep 17 00:00:00 2001 From: Hamza Abdelhedi Date: Sun, 30 Aug 2026 17:38:37 -0400 Subject: [PATCH 06/11] CI install shared lockfile checker with uv --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f7923103..96fd49bd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -106,7 +106,7 @@ jobs: fetch-depth: 1 persist-credentials: false - name: Install shared lockfile checker - run: python -m pip install -e "$GITHUB_WORKSPACE/.ci/mne-tools" + run: uv pip install -e "$GITHUB_WORKSPACE/.ci/mne-tools" - name: Validate lower-bound lockfile run: | python -m mne_tools.check_lockfile \ From 8b291dc239c5f4f86268d70d11d8fedd708642d9 Mon Sep 17 00:00:00 2001 From: Hamza Abdelhedi Date: Sun, 30 Aug 2026 17:49:52 -0400 Subject: [PATCH 07/11] MAINT raise Python 3.12 dependency floors --- pyproject.toml | 4 ++-- tools/pylock.ci-old.toml | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d534f572..5741d818 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "numpy>=1.26,<3", "scipy>=1.13", "scikit-learn>=1.5", - "joblib>=1.2", + "joblib>=1.4", ] [project.urls] @@ -80,7 +80,7 @@ test = [ "pytest-cov>=4.1.0", "pytest-timeout>=2.2.0", "pandas>=2.1.1", - "seaborn>=0.12", + "seaborn>=0.13", { include-group = "lockfile_extras" }, ] diff --git a/tools/pylock.ci-old.toml b/tools/pylock.ci-old.toml index c8d41a07..2fb84b0a 100644 --- a/tools/pylock.ci-old.toml +++ b/tools/pylock.ci-old.toml @@ -76,9 +76,9 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f [[packages]] name = "joblib" -version = "1.2.0" -sdist = { url = "https://files.pythonhosted.org/packages/45/dd/a5435a6902d6315241c48a5343e6e6675b007e05d3738ed97a7a47864e53/joblib-1.2.0.tar.gz", upload-time = 2022-09-16T10:01:07Z, size = 313200, hashes = { sha256 = "e1cee4a79e4af22881164f218d4311f60074197fb707e082e803b61f6d137018" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/91/d4/3b4c8e5a30604df4c7518c562d4bf0502f2fa29221459226e140cf846512/joblib-1.2.0-py3-none-any.whl", upload-time = 2022-09-16T10:01:04Z, size = 297969, hashes = { sha256 = "091138ed78f800342968c523bdde947e7a305b8594b910a0fea2ab83c3c6d385" } }] +version = "1.4.0" +sdist = { url = "https://files.pythonhosted.org/packages/7c/c3/94c9e4886e8f33832690ab48fdac4a121a7bfec3e2c044c9f2762aa9068e/joblib-1.4.0.tar.gz", upload-time = 2024-04-08T15:08:19Z, size = 2115863, hashes = { sha256 = "1eb0dc091919cd384490de890cb5dfd538410a6d4b3b54eef09fb8c50b409b1c" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/ae/e2/4dea6313ef2b38442fccbbaf4017e50a6c3c8a50e8ee9b512783e5c90409/joblib-1.4.0-py3-none-any.whl", upload-time = 2024-04-08T15:08:14Z, size = 301185, hashes = { sha256 = "42942470d4062537be4d54c83511186da1fc14ba354961a2114da91efa9a4ed7" } }] [[packages]] name = "kiwisolver" @@ -208,9 +208,9 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/87/8c/97e545034c94d0b [[packages]] name = "seaborn" -version = "0.12.0" -sdist = { url = "https://files.pythonhosted.org/packages/0a/87/9d713b302b7319f58e76f37fb2308377035c594b76bd50fe3696dad3a27e/seaborn-0.12.0.tar.gz", upload-time = 2022-09-06T03:04:03Z, size = 1407601, hashes = { sha256 = "893f17292d8baca616c1578ddb58eb25c72d622f54fc5ee329c8207dc9b57b23" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/c2/03/14991c1f18422eb640f6fe6eadf9a675bb21d9339236d64c3d12bd0eb1a4/seaborn-0.12.0-py3-none-any.whl", upload-time = 2022-09-06T03:04:01Z, size = 285116, hashes = { sha256 = "cbeff3deef7c2515aa0af99b2c7e02dc5bf8b42c936a74d8e4b416905b549db0" } }] +version = "0.13.0" +sdist = { url = "https://files.pythonhosted.org/packages/06/6f/caf0741c5787358b0efba3b4db7f8235e3a48e719ad2444bbd51485f966c/seaborn-0.13.0.tar.gz", upload-time = 2023-09-29T18:58:36Z, size = 1455480, hashes = { sha256 = "0e76abd2ec291c655b516703c6a022f0fd5afed26c8e714e8baef48150f73598" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/7b/e5/83fcd7e9db036c179e0352bfcd20f81d728197a16f883e7b90307a88e65e/seaborn-0.13.0-py3-none-any.whl", upload-time = 2023-09-29T18:58:33Z, size = 294583, hashes = { sha256 = "70d740828c48de0f402bb17234e475eda687e3c65f4383ea25d0cc4728f7772e" } }] [[packages]] name = "six" From 82a528417dab0289ee7f45545f525f55ac165ea0 Mon Sep 17 00:00:00 2001 From: Hamza Abdelhedi Date: Sun, 30 Aug 2026 18:13:16 -0400 Subject: [PATCH 08/11] CI install shared checker without nested checkout --- .github/workflows/autofix.yml | 19 ++++++++++--------- .github/workflows/tests.yml | 12 +++--------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 54b67ab2..e0a6f66e 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -31,16 +31,10 @@ jobs: python-version: "3.12" cache: pip - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 - - name: Checkout shared MNE-tools checker - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - repository: mne-tools/mne-tools - ref: 2e7c4b5469ef311a169e16c7f97cd0ce68508a61 - path: .ci/mne-tools - fetch-depth: 1 - persist-credentials: false - name: Install shared lockfile checker - run: python -m pip install -e "$GITHUB_WORKSPACE/.ci/mne-tools" + run: >- + python -m pip install + "mne-tools @ git+https://github.com/mne-tools/mne-tools.git@2e7c4b5469ef311a169e16c7f97cd0ce68508a61" - name: Install maintenance tools run: | python -m pip install --upgrade pip @@ -82,6 +76,13 @@ jobs: --groups lockfile_extras - name: Run repository hooks run: prek run --all-files + - name: Verify autofix working tree + run: | + git status --short + if git status --short | grep -F -- '.ci/mne-tools'; then + echo 'Unexpected nested mne-tools repository in the working tree.' >&2 + exit 1 + fi - name: Apply fixes through autofix.ci if: success() || failure() uses: autofix-ci/action@c5b2d67aa2274e7b5a18224e8171550871fc7e4a # v1.3.4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 96fd49bd..c0abd59e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -97,16 +97,10 @@ jobs: cache-dependency-glob: | pyproject.toml tools/pylock.ci-old.toml - - name: Checkout shared MNE-tools checker - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - repository: mne-tools/mne-tools - ref: 2e7c4b5469ef311a169e16c7f97cd0ce68508a61 - path: .ci/mne-tools - fetch-depth: 1 - persist-credentials: false - name: Install shared lockfile checker - run: uv pip install -e "$GITHUB_WORKSPACE/.ci/mne-tools" + run: >- + uv pip install + "mne-tools @ git+https://github.com/mne-tools/mne-tools.git@2e7c4b5469ef311a169e16c7f97cd0ce68508a61" - name: Validate lower-bound lockfile run: | python -m mne_tools.check_lockfile \ From 410c2682f644af553efe1d11d768bd29829a6433 Mon Sep 17 00:00:00 2001 From: Hamza Abdelhedi Date: Sun, 30 Aug 2026 18:47:43 -0400 Subject: [PATCH 09/11] DOC polish release instructions --- RELEASING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 850bb861..a30eaadd 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -130,8 +130,8 @@ After the GitHub Release is published: 3. `Publish to PyPI` waits for the `pypi` GitHub Environment rules, if any. A required reviewer may approve the deployment. 4. The job downloads `release-dists` and invokes - `pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33` # v1.14.2 - with GitHub OIDC. It does not + `pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33` + (v1.14.2) with GitHub OIDC. It does not rebuild the package. The workflow uses no PyPI API token or stored upload credentials, and it does From 5b637c9c28504320623dca2581cc5aa76b463ac0 Mon Sep 17 00:00:00 2001 From: Hamza Abdelhedi Date: Sun, 30 Aug 2026 18:58:21 -0400 Subject: [PATCH 10/11] Revert "Merge pull request #99 from mne-tools/codex/maintenance-automation" This reverts commit f45795035db669fa6b8d0f32c37e86e6f48ea719, reversing changes made to bb520af364b573cb16874c2f96e37446784186ee. --- .github/PULL_REQUEST_TEMPLATE.md | 6 +- .../rename_towncrier/rename_towncrier.py | 59 ++++++++++++ .github/dependabot.yml | 19 ++-- .github/workflows/autofix.yml | 88 ----------------- .github/workflows/check_changelog.yml | 27 ------ .github/workflows/dependency-review.yml | 21 ----- .github/workflows/docs.yml | 26 ++--- .github/workflows/release.yml | 14 +-- .github/workflows/tests.yml | 66 +++++++------ .pre-commit-config.yaml | 28 +++--- .spin/cmds.py | 88 ----------------- .yamllint.yml | 9 -- CONTRIBUTING.md | 59 +++++------- README.md | 4 +- RELEASING.md | 3 +- SECURITY.md | 13 --- codecov.yml | 4 +- docs/changes/README.md | 15 +-- docs/changes/devel/99.misc.rst | 3 - docs/changes/devel/99.removal.rst | 2 - docs/getting-started.rst | 2 +- mne_denoise/_logging.py | 8 +- pyproject.toml | 50 +++++----- scripts/check_dist.py | 12 +-- scripts/rename_towncrier.py | 94 ------------------- tests/test_rename_towncrier.py | 78 --------------- tools/pylock.ci-old.toml | 60 ++++++------ 27 files changed, 232 insertions(+), 626 deletions(-) create mode 100644 .github/actions/rename_towncrier/rename_towncrier.py delete mode 100644 .github/workflows/autofix.yml delete mode 100644 .github/workflows/check_changelog.yml delete mode 100644 .github/workflows/dependency-review.yml delete mode 100644 .spin/cmds.py delete mode 100644 .yamllint.yml delete mode 100644 SECURITY.md delete mode 100644 docs/changes/devel/99.misc.rst delete mode 100644 docs/changes/devel/99.removal.rst delete mode 100644 scripts/rename_towncrier.py delete mode 100644 tests/test_rename_towncrier.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index af4154b1..ca744f59 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -25,8 +25,8 @@ -- [ ] My code follows the code style of this project (`spin lint`) +- [ ] My code follows the code style of this project (`ruff check .`, `ruff format .`) - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have run the full test suite and all tests pass -- [ ] I have updated the documentation accordingly (`spin docs` builds without warnings) -- [ ] I have added a numbered Towncrier fragment in `docs/changes/devel/` (if applicable) +- [ ] I have updated the documentation accordingly (`make -C docs html` builds without warnings) +- [ ] I have added an entry to `CHANGELOG.md` (if applicable) diff --git a/.github/actions/rename_towncrier/rename_towncrier.py b/.github/actions/rename_towncrier/rename_towncrier.py new file mode 100644 index 00000000..89be8990 --- /dev/null +++ b/.github/actions/rename_towncrier/rename_towncrier.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +# Authors: The MNE-Python contributors. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. +# Copied from mne-python: +# https://github.com/mne-tools/mne-python/blob/main/.github/actions/rename_towncrier/rename_towncrier.py + +import json +import os +import re +import subprocess +import sys +from pathlib import Path +from tomllib import loads + +from github import Github + +event_name = os.getenv("GITHUB_EVENT_NAME", "pull_request") +if not event_name.startswith("pull_request"): + print(f"No-op for {event_name}") + sys.exit(0) +if "GITHUB_EVENT_PATH" in os.environ: + with open(os.environ["GITHUB_EVENT_PATH"], encoding="utf-8") as fin: + event = json.load(fin) + pr_num = event["number"] + basereponame = event["pull_request"]["base"]["repo"]["full_name"] + real = True +else: # local testing + pr_num = 12318 # added some towncrier files + basereponame = "mne-tools/mne-python" + real = False + +g = Github(os.environ.get("GITHUB_TOKEN")) +baserepo = g.get_repo(basereponame) + +# Grab config from upstream's default branch +toml_cfg = loads(Path("pyproject.toml").read_text("utf-8")) + +config = toml_cfg["tool"]["towncrier"] +pr = baserepo.get_pull(pr_num) +modified_files = [f.filename for f in pr.get_files()] + +# Get types from config +types = [ent["directory"] for ent in toml_cfg["tool"]["towncrier"]["type"]] +type_pipe = "|".join(types) + +# Get files that potentially match the types +directory = toml_cfg["tool"]["towncrier"]["directory"] +assert directory.endswith("/"), directory + +file_re = re.compile(rf"^{directory}({type_pipe})\.rst$") +found_stubs = [f for f in modified_files if file_re.match(f)] +for stub in found_stubs: + fro = stub + to = file_re.sub(rf"{directory}{pr_num}.\1.rst", fro) + print(f"Renaming {fro} to {to}") + if real: + subprocess.check_call(["mv", fro, to]) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6849bfef..4079c986 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,20 +1,13 @@ version: 2 updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" - day: "monday" - time: "05:17" - timezone: "UTC" - cooldown: - default-days: 7 - commit-message: - prefix: "[dependabot]" - groups: - github-actions: - patterns: - - "*" - labels: - - "no-changelog-entry-needed" open-pull-requests-limit: 10 diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml deleted file mode 100644 index e0a6f66e..00000000 --- a/.github/workflows/autofix.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: autofix.ci - -on: # yamllint disable-line rule:truthy - pull_request: - branches: [main] - types: [opened, synchronize, labeled, unlabeled] - -concurrency: - group: ${{ github.workflow }}-${{ github.event.number || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - pull-requests: read - -jobs: - autofix: - name: Apply repository fixes - runs-on: ubuntu-latest - timeout-minutes: 20 - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: "3.12" - cache: pip - - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 - - name: Install shared lockfile checker - run: >- - python -m pip install - "mne-tools @ git+https://github.com/mne-tools/mne-tools.git@2e7c4b5469ef311a169e16c7f97cd0ce68508a61" - - name: Install maintenance tools - run: | - python -m pip install --upgrade pip - python -m pip install --group lint --group changelog - - name: Number the changelog fragment - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - run: python scripts/rename_towncrier.py --pr-number "$PR_NUMBER" - - name: Detect dependency changes - id: changed - env: - GH_REPO: ${{ github.repository }} - GH_PR_NUMBER: ${{ github.event.pull_request.number }} - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - changed_files="$(gh api --paginate \ - --header 'Accept: application/vnd.github+json' \ - "repos/${GH_REPO}/pulls/${GH_PR_NUMBER}/files?per_page=100" \ - --jq '.[].filename')" - if grep -Eq '^(pyproject\.toml|tools/pylock\.ci-old\.toml|\.github/workflows/autofix\.yml)$' <<<"$changed_files"; then - echo "dependencies=true" >> "$GITHUB_OUTPUT" - else - echo "dependencies=false" >> "$GITHUB_OUTPUT" - fi - - name: Regenerate lower-bound lockfile - if: steps.changed.outputs.dependencies == 'true' - run: >- - uv pip compile pyproject.toml --python "3.12" - --python-platform "x86_64-unknown-linux-gnu" --group test - --group lockfile_extras --resolution lowest-direct --format pylock.toml - --output-file tools/pylock.ci-old.toml --no-cache - - name: Validate lower-bound lockfile - if: steps.changed.outputs.dependencies == 'true' - run: | - python -m mne_tools.check_lockfile \ - "$GITHUB_WORKSPACE" \ - tools/pylock.ci-old.toml \ - --groups lockfile_extras - - name: Run repository hooks - run: prek run --all-files - - name: Verify autofix working tree - run: | - git status --short - if git status --short | grep -F -- '.ci/mne-tools'; then - echo 'Unexpected nested mne-tools repository in the working tree.' >&2 - exit 1 - fi - - name: Apply fixes through autofix.ci - if: success() || failure() - uses: autofix-ci/action@c5b2d67aa2274e7b5a18224e8171550871fc7e4a # v1.3.4 diff --git a/.github/workflows/check_changelog.yml b/.github/workflows/check_changelog.yml deleted file mode 100644 index c072f45f..00000000 --- a/.github/workflows/check_changelog.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Changelog - -on: # yamllint disable-line rule:truthy - pull_request: - branches: [main] - types: [opened, synchronize, labeled, unlabeled] - -permissions: - contents: read - pull-requests: read - -jobs: - changelog: - name: Check changelog entry - runs-on: ubuntu-latest - timeout-minutes: 10 - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - uses: scientific-python/action-towncrier-changelog@165df35cfb3ff4b5bfea7645c4b923c6e8c7b54a # v2.0.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BOT_USERNAME: changelog-bot diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml deleted file mode 100644 index cdfef1a7..00000000 --- a/.github/workflows/dependency-review.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Dependency review - -on: # yamllint disable-line rule:truthy - pull_request: - branches: [main] - -permissions: - contents: read - -jobs: - dependency-review: - name: Dependency review - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 - with: - fail-on-severity: high diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 78eea5c4..3551682b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,6 +1,6 @@ name: Docs -on: # yamllint disable-line rule:truthy +on: push: branches: [main] pull_request: @@ -27,11 +27,11 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - uses: actions/setup-python@v7 with: python-version: "3.14" cache: pip @@ -52,7 +52,7 @@ jobs: run: python -m pip check - name: Restore documentation datasets id: docs-data-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/cache/restore@v6 with: path: ${{ env.MNE_DATA }} key: docs-data-${{ runner.os }}-${{ hashFiles('scripts/prefetch_docs_data.py', 'pyproject.toml') }} @@ -66,7 +66,7 @@ jobs: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/cache/save@v6 with: path: ${{ env.MNE_DATA }} key: docs-data-${{ runner.os }}-${{ hashFiles('scripts/prefetch_docs_data.py', 'pyproject.toml') }} @@ -83,7 +83,7 @@ jobs: test -s docs/_build/html/auto_examples/zapline/plot_03_epoched_data.html echo "$(find docs/_build/html -name '*.html' | wc -l) HTML pages built" - name: Upload documentation artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@v7 with: name: docs-html-stable-mne path: docs/_build/html/ @@ -99,11 +99,11 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - uses: actions/setup-python@v7 with: python-version: "3.14" cache: pip @@ -112,7 +112,7 @@ jobs: python -m pip install --upgrade pip python -m pip install -e . --group doc python -c "import mne; print(mne.__version__)" > "$RUNNER_TEMP/docs-mne-stable-version" - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7 with: repository: mne-tools/mne-python path: .ci/mne-python @@ -142,7 +142,7 @@ jobs: raise SystemExit("MNE main installation did not replace stable MNE") PY - name: Restore documentation datasets - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + uses: actions/cache/restore@v6 with: path: ${{ env.MNE_DATA }} key: docs-data-${{ runner.os }}-${{ hashFiles('scripts/prefetch_docs_data.py', 'pyproject.toml') }} @@ -165,7 +165,7 @@ jobs: test -s docs/_build/html/auto_examples/zapline/plot_03_epoched_data.html echo "$(find docs/_build/html -name '*.html' | wc -l) HTML pages built" - name: Upload documentation artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@v7 with: name: docs-html-mne-main path: docs/_build/html/ @@ -181,12 +181,12 @@ jobs: contents: write steps: - name: Download stable-MNE documentation - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + uses: actions/download-artifact@v8 with: name: docs-html-stable-mne path: docs/_build/html/ - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@1ef5a1b1df4c63fe21a2242edbee6cac921ece01 # v4.1.0 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: docs/_build/html/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c57e4620..7bf86c1b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ name: Release -on: # yamllint disable-line rule:truthy +on: release: types: [published] push: @@ -20,13 +20,13 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - uses: actions/setup-python@v7 with: - python-version: "3.12" + python-version: "3.11" - name: Install build dependencies run: | python -m pip install --upgrade pip @@ -108,7 +108,7 @@ jobs: "$wheel_test/bin/python" "$GITHUB_WORKSPACE/scripts/check_base_install.py" "$wheel_test/bin/python" -m pip check - name: Upload release distributions - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@v7 with: name: release-dists path: dist/ @@ -127,11 +127,11 @@ jobs: name: pypi url: https://pypi.org/p/mne-denoise steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@v8 with: name: release-dists path: dist/ - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + uses: pypa/gh-action-pypi-publish@release/v1 with: print-hash: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c0abd59e..a82157ad 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,6 @@ name: Tests -on: # yamllint disable-line rule:truthy +on: push: branches: [main] pull_request: @@ -26,11 +26,11 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - uses: actions/setup-python@v7 with: python-version: "3.14" cache: pip @@ -38,11 +38,15 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install --group lint - - name: Run repository checks - run: spin lint + - name: Run ruff check + run: ruff check . + - name: Run ruff format check + run: ruff format --check . + - name: Run pre-commit + run: pre-commit run --all-files base-install: - name: Base install / Python 3.12 + name: Base install / Python 3.11 if: github.event_name != 'schedule' runs-on: ubuntu-latest timeout-minutes: 10 @@ -50,13 +54,13 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - uses: actions/setup-python@v7 with: - python-version: "3.12" + python-version: "3.11" - name: Validate base installation run: | base_python="$RUNNER_TEMP/mne-denoise-base/bin/python" @@ -85,32 +89,28 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false - - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + - uses: astral-sh/setup-uv@v10.0.1 with: version: ">=0.9" - python-version: "3.12" + python-version: "3.11" activate-environment: true cache-dependency-glob: | pyproject.toml tools/pylock.ci-old.toml - - name: Install shared lockfile checker - run: >- - uv pip install - "mne-tools @ git+https://github.com/mne-tools/mne-tools.git@2e7c4b5469ef311a169e16c7f97cd0ce68508a61" - name: Validate lower-bound lockfile - run: | - python -m mne_tools.check_lockfile \ - "$GITHUB_WORKSPACE" \ - tools/pylock.ci-old.toml \ - --groups lockfile_extras + uses: mne-tools/mne-tools/actions/check-lockfile@main + with: + project-root: ${{ github.workspace }} + lockfile-path: tools/pylock.ci-old.toml + groups: lockfile_extras - name: Restore lower-bound Python environment run: | minimum_env="$RUNNER_TEMP/mne-denoise-minimum" - uv venv --python 3.12 "$minimum_env" + uv venv --python 3.11 "$minimum_env" echo "VIRTUAL_ENV=$minimum_env" >> "$GITHUB_ENV" echo "$minimum_env/bin" >> "$GITHUB_PATH" - name: Install lower-bound environment @@ -120,7 +120,7 @@ jobs: uv pip install --python "$minimum_python" pip uv pip install --python "$minimum_python" -e . --no-deps - name: Check declared lower bounds - uses: mne-tools/mne-tools/actions/check-environment@2e7c4b5469ef311a169e16c7f97cd0ce68508a61 # audited mne-tools main + uses: mne-tools/mne-tools/actions/check-environment@main with: project-root: ${{ github.workspace }} groups: lockfile_extras @@ -156,6 +156,10 @@ jobs: fail-fast: false matrix: include: + - os: ubuntu-latest + python-version: "3.12" + name: Python 3.12 + coverage: "false" - os: ubuntu-latest python-version: "3.13" name: Python 3.13 @@ -176,11 +180,11 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} cache: pip @@ -227,7 +231,7 @@ jobs: run: pytest --cov=mne_denoise --cov-report=xml -q - name: Upload coverage to Codecov if: matrix.coverage == 'true' - uses: codecov/codecov-action@a99c28d3f0da835de33ff2feb2e15691c7b9641f # v7.0.0 + uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml @@ -242,11 +246,11 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - uses: actions/setup-python@v7 with: python-version: "3.14" cache: pip @@ -255,7 +259,7 @@ jobs: python -m pip install --upgrade pip python -m pip install -e . --group test python -c "import mne; print(mne.__version__)" > "$RUNNER_TEMP/mne-stable-version" - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7 with: repository: mne-tools/mne-python path: .ci/mne-python @@ -315,11 +319,11 @@ jobs: run: shell: bash steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + - uses: actions/setup-python@v7 with: python-version: "3.14" cache: pip diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 514a94de..41d44890 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,14 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: aab412d509121cb5f7533134b7e67f9fab59c682 # frozen: v0.16.4 + rev: v0.16.0 hooks: - - id: ruff-check + - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format types_or: [python, pyi, jupyter, markdown] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 + rev: v5.0.0 hooks: - id: check-added-large-files args: [--maxkb=1000] @@ -25,20 +25,20 @@ repos: - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - - repo: https://github.com/adrienverge/yamllint - rev: cba56bcde1fdd01c1deb3f945e69764c291a6530 # frozen: v1.38.0 + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 hooks: - - id: yamllint - args: [--strict, -c, .yamllint.yml] - - - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: 451b56af716f9f0d0c2b816503a3fd0cf8b036fa # frozen: v1.29.0 - hooks: - - id: zizmor - args: [--no-progress, --min-severity=medium] + - id: prettier + types_or: [yaml, json] + exclude: ^docs/ - repo: https://github.com/codespell-project/codespell - rev: 57b21406f092110c18776e39b0bda50d37c945c8 # frozen: v2.4.3 + rev: v2.2.6 hooks: - id: codespell + additional_dependencies: [tomli] args: [--skip, "*.html,*.css,*.js,*.svg,*.lock,*.ipynb"] + +ci: + autoupdate_schedule: quarterly + skip: [codespell] diff --git a/.spin/cmds.py b/.spin/cmds.py deleted file mode 100644 index 26f12d36..00000000 --- a/.spin/cmds.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Project commands exposed through Spin.""" - -from __future__ import annotations - -import os -import shutil -import subprocess -import sys -import tempfile -from pathlib import Path - -import click - - -def _run( - *command: str, - args: tuple[str, ...] = (), - env: dict[str, str] | None = None, -) -> None: - """Run a project command and propagate its exit status.""" - subprocess.run([*command, *args], check=True, env=env) - - -@click.command(context_settings={"ignore_unknown_options": True}) -@click.argument("args", nargs=-1, type=click.UNPROCESSED) -def test(args: tuple[str, ...]) -> None: - """Run the test suite, optionally forwarding pytest arguments.""" - _run("pytest", "-q", args=args) - - -@click.command() -def lint() -> None: - """Run all repository hooks.""" - _run("prek", "run", "--all-files") - - -@click.command() -def docs() -> None: - """Build the documentation with warnings treated as errors.""" - with tempfile.TemporaryDirectory(prefix="mne-denoise-docs-") as temp_dir: - temp_root = Path(temp_dir) - environment = os.environ.copy() - environment.update( - { - "MPLBACKEND": "Agg", - "MPLCONFIGDIR": str(temp_root / "mplconfig"), - "HOME": str(temp_root / "home"), - "MNE_HOME": str(temp_root / "mne"), - "NUMBA_CACHE_DIR": str(temp_root / "numba"), - "MNE_DONTWRITE_HOME": "true", - } - ) - for directory in ("mplconfig", "home", "mne", "numba"): - (temp_root / directory).mkdir() - _run( - sys.executable, - "-m", - "sphinx", - "-b", - "html", - "-W", - "--keep-going", - "docs", - "docs/_build/html", - env=environment, - ) - - -@click.command() -def build() -> None: - """Build and validate clean Python distribution artifacts.""" - dist = Path("dist") - if dist.is_dir(): - shutil.rmtree(dist) - elif dist.exists(): - dist.unlink() - _run(sys.executable, "-m", "build") - artifacts = tuple(str(path) for path in sorted(dist.iterdir())) - _run(sys.executable, "-m", "twine", "check", "--strict", args=artifacts) - _run(sys.executable, "scripts/check_dist.py", "dist") - - -@click.command() -def check() -> None: - """Run hooks, tests, and distribution validation.""" - _run(sys.executable, "-m", "spin", "lint") - _run(sys.executable, "-m", "spin", "test") - _run(sys.executable, "-m", "spin", "build") diff --git a/.yamllint.yml b/.yamllint.yml deleted file mode 100644 index 78b78dd2..00000000 --- a/.yamllint.yml +++ /dev/null @@ -1,9 +0,0 @@ -extends: default - -rules: - line-length: disable - document-start: disable - new-lines: - type: platform - indentation: - indent-sequences: consistent diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5812b32d..0c73f839 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ This project follows the [MNE-Python Code of Conduct](https://github.com/mne-too ### Prerequisites -- Python 3.12 or higher +- Python 3.11 or higher - Git - A GitHub account @@ -66,8 +66,8 @@ python -m pip install --upgrade pip # Install in editable mode with the development dependency group python -m pip install -e . --group dev -# Install the repository hooks -prek install +# Install pre-commit hooks +pre-commit install ``` ### Using conda @@ -80,27 +80,9 @@ conda activate mne-denoise # Install in editable mode python -m pip install --upgrade pip python -m pip install -e . --group dev -prek install +pre-commit install ``` -### Development commands - -The development dependency group includes `uv`-compatible project tooling, -`spin`, and `prek`. Spin is the project task interface, not an environment -manager: - -```bash -spin test # run pytest -spin test -- -k asr # forward arguments to pytest -spin lint # run every repository hook -spin docs # build docs with warnings as errors -spin build # build and validate distributions -spin check # lint, test, and validate distributions -``` - -Use `prek install` once to install the local Git hooks, and -`prek run --all-files` to run them directly. - ## Workflow ### 1. Create a Branch @@ -153,11 +135,7 @@ git rebase upstream/main We use [towncrier](https://towncrier.readthedocs.io/) to manage our changelog. This prevents merge conflicts and ensures standardized release notes. -When you create a Pull Request, add a changelog fragment in -`docs/changes/devel/`. Name it `..rst`, for example -`123.feature.rst`. During local development, an unnumbered `feature.rst` or -`bugfix.rst` can be renamed with `python scripts/rename_towncrier.py ---pr-number 123`; the pull-request automation performs the same conversion. +When you create a Pull Request, please add a changelog entry file in `docs/changes/devel/`. The file name should be the change type (e.g., `feature.rst`, `bugfix.rst`). For detailed instructions and available types, see [docs/changes/README.md](https://github.com/mne-tools/mne-denoise/blob/main/docs/changes/README.md). @@ -169,14 +147,20 @@ We use **Ruff** for linting and formatting, configured to follow PEP 8 with NumP ### Automatic Formatting -Repository hooks automatically format your code on commit. To run them manually: +Pre-commit hooks will automatically format your code on commit. To run manually: ```bash -# Run the complete repository quality suite -spin lint +# Check for linting errors +ruff check . + +# Auto-fix linting errors +ruff check . --fix + +# Format code +ruff format . -# Run hooks directly -prek run --all-files +# Run all pre-commit hooks +pre-commit run --all-files ``` ### Docstring Style @@ -307,8 +291,8 @@ Documentation is built with Sphinx and hosted on GitHub Pages. ### Building Docs Locally ```bash -# Build HTML documentation with warnings as errors -spin docs +# Build HTML documentation +make -C docs html # Populate the real-data cache before a full gallery build python scripts/prefetch_docs_data.py @@ -416,12 +400,13 @@ a documented deprecation process. Before submitting, ensure: -- [ ] Code follows the project style (`spin lint` passes) +- [ ] Code follows the project style (`ruff check .` passes) +- [ ] Code is formatted (`ruff format .` produces no changes) - [ ] All tests pass (`pytest` exits cleanly) - [ ] New code has tests with good coverage - [ ] Documentation is updated if needed -- [ ] Documentation builds cleanly (`spin docs`) -- [ ] A numbered Towncrier fragment is included for user-facing changes +- [ ] Documentation builds cleanly (`make -C docs html`) +- [ ] CHANGELOG.md is updated for user-facing changes - [ ] Commit messages are clear and descriptive ## Issue Guidelines diff --git a/README.md b/README.md index f2f16202..3ac3314f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # mne-denoise -[![Tests](https://github.com/mne-tools/mne-denoise/actions/workflows/tests.yml/badge.svg)](https://github.com/mne-tools/mne-denoise/actions/workflows/tests.yml) +[![CI](https://github.com/mne-tools/mne-denoise/actions/workflows/ci.yml/badge.svg)](https://github.com/mne-tools/mne-denoise/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/mne-tools/mne-denoise/branch/main/graph/badge.svg)](https://codecov.io/gh/mne-tools/mne-denoise) [![PyPI version](https://img.shields.io/pypi/v/mne-denoise.svg)](https://pypi.org/project/mne-denoise/) [![Python versions](https://img.shields.io/pypi/pyversions/mne-denoise.svg)](https://pypi.org/project/mne-denoise/) @@ -198,7 +198,7 @@ git clone https://github.com//mne-denoise.git cd mne-denoise python -m pip install --upgrade pip python -m pip install -e . --group dev -prek install +pre-commit install ``` ## References diff --git a/RELEASING.md b/RELEASING.md index a30eaadd..9f2cd7c7 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -130,8 +130,7 @@ After the GitHub Release is published: 3. `Publish to PyPI` waits for the `pypi` GitHub Environment rules, if any. A required reviewer may approve the deployment. 4. The job downloads `release-dists` and invokes - `pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33` - (v1.14.2) with GitHub OIDC. It does not + `pypa/gh-action-pypi-publish@release/v1` with GitHub OIDC. It does not rebuild the package. The workflow uses no PyPI API token or stored upload credentials, and it does diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index eebf7f28..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,13 +0,0 @@ -# Security policy - -## Reporting a vulnerability - -Please report suspected vulnerabilities privately through the repository's -[GitHub security advisories page](https://github.com/mne-tools/mne-denoise/security/advisories/new) -rather than opening a public issue. Include the affected version, a minimal -reproduction, and any relevant impact or mitigation details. - -We will acknowledge reports as soon as practical and coordinate a fix and -disclosure timeline with the reporter. Security fixes are provided in the -latest supported release; users should upgrade promptly when a security -advisory is published. diff --git a/codecov.yml b/codecov.yml index 088d1892..bd868fff 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,8 +2,8 @@ coverage: status: project: default: - target: 80% # Global target - threshold: 1% # Allow 1% drop + target: 80% # Global target + threshold: 1% # Allow 1% drop patch: default: # New code should be well covered, but need not exactly match the diff --git a/docs/changes/README.md b/docs/changes/README.md index 5d284058..747724a9 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -6,18 +6,9 @@ We use `towncrier` to manage our changelog. This ensures that changes are docume When you make a change (feature, bugfix, documentation update), you should add a fragment file to the `docs/changes/devel/` directory. -The filename should include the pull request number and the change type: -`..rst`, such as `123.bugfix.rst`. For a local draft, create an -unnumbered stub such as `bugfix.rst` and run: +The filename should be the type of change and the extension `.rst`. The PR number will be added automatically. -```bash -python scripts/rename_towncrier.py --pr-number 123 -``` - -The pull-request automation performs this rename when needed. Do not edit -`CHANGELOG.md` in a pull request. - -Format: `..rst` +Format: `.rst` ### Available types: @@ -29,7 +20,7 @@ Format: `..rst` ## Example -If you fixed a bug in PR 123, create a file `docs/changes/devel/123.bugfix.rst`: +If you fixed a bug in a PR, create a file `docs/changes/devel/bugfix.rst`: ```rst Fixed a bug where the ZapLine algorithm would crash on empty data. diff --git a/docs/changes/devel/99.misc.rst b/docs/changes/devel/99.misc.rst deleted file mode 100644 index 3d64556d..00000000 --- a/docs/changes/devel/99.misc.rst +++ /dev/null @@ -1,3 +0,0 @@ -Developer tooling and CI maintenance now use Spin, prek, immutable GitHub -Actions, automated lower-bound dependency validation, dependency review, and -security-focused repository checks. diff --git a/docs/changes/devel/99.removal.rst b/docs/changes/devel/99.removal.rst deleted file mode 100644 index dddc5d84..00000000 --- a/docs/changes/devel/99.removal.rst +++ /dev/null @@ -1,2 +0,0 @@ -Python 3.11 support has been removed. Python 3.12 is now the minimum -supported Python version. diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 4e14f3fa..5e384d1a 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -24,7 +24,7 @@ dependency group: python -m pip install --upgrade pip python -m pip install -e . --group dev - prek install + pre-commit install Basic usage ----------- diff --git a/mne_denoise/_logging.py b/mne_denoise/_logging.py index da10a75f..660b0b32 100644 --- a/mne_denoise/_logging.py +++ b/mne_denoise/_logging.py @@ -16,7 +16,7 @@ from contextvars import ContextVar from functools import wraps from numbers import Integral -from typing import Any +from typing import Any, TypeVar logger = logging.getLogger("mne_denoise") @@ -26,6 +26,8 @@ default=_UNSET, ) +_F = TypeVar("_F", bound=Callable[..., Any]) + def _level_from_verbose(verbose: bool | str | int | None) -> int | None: """Resolve one MNE-style verbosity value to a logging level.""" @@ -71,7 +73,7 @@ def use_log_level(verbose: bool | str | int | None) -> Iterator[None]: _active_verbose_scope.reset(token) -def verbose[**P, T](function: Callable[P, T]) -> Callable[P, T]: +def verbose(function: _F) -> _F: """Decorate a public operation with a temporary ``verbose`` override. The decorator accepts the same forms as MNE-Python's ``@verbose``. An @@ -96,4 +98,4 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: return function(*args, **kwargs) - return wrapper + return wrapper # type: ignore[return-value] diff --git a/pyproject.toml b/pyproject.toml index 5741d818..d3ffc9bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "mne-denoise" dynamic = ["version"] description = "Artifact removal and signal denoising for EEG and MEG." readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.11" license = "BSD-3-Clause" license-files = ["LICENSE"] authors = [ @@ -37,6 +37,7 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", @@ -47,7 +48,7 @@ dependencies = [ "numpy>=1.26,<3", "scipy>=1.13", "scikit-learn>=1.5", - "joblib>=1.4", + "joblib>=1.2", ] [project.urls] @@ -79,8 +80,8 @@ test = [ "pytest>=7.4.0", "pytest-cov>=4.1.0", "pytest-timeout>=2.2.0", - "pandas>=2.1.1", - "seaborn>=0.13", + "pandas>=1.5", + "seaborn>=0.12", { include-group = "lockfile_extras" }, ] @@ -102,8 +103,12 @@ build = [ ] lint = [ - "prek>=0.5", - "spin>=0.18", + "ruff>=0.16.0", + "pre-commit>=3.6.0", +] + +typecheck = [ + "mypy>=1.8.0", ] changelog = [ @@ -116,6 +121,7 @@ dev = [ { include-group = "doc" }, { include-group = "build" }, { include-group = "lint" }, + { include-group = "typecheck" }, { include-group = "changelog" }, ] @@ -125,7 +131,7 @@ raw-options = { version_scheme = "guess-next-dev" } [tool.ruff] line-length = 88 -target-version = "py312" +target-version = "py311" [tool.ruff.lint] select = [ @@ -193,9 +199,18 @@ exclude_lines = [ "raise NotImplementedError", ] +[tool.mypy] +python_version = "3.11" +warn_unused_configs = true +ignore_missing_imports = true +show_error_codes = true +pretty = true +warn_redundant_casts = true +warn_unused_ignores = true + [tool.codespell] skip = "*.html,*.css,*.js,*.svg,*.lock,.git,__pycache__,*.egg-info,build,dist,docs/_build,*.map" -ignore-words-list = "nd,ot,fro,sems,pre-select,Disjointness" +ignore-words-list = "nd,ot,fro" [tool.towncrier] package = "mne_denoise" @@ -206,13 +221,6 @@ issue_format = "[#{issue}](https://github.com/mne-tools/mne-denoise/issues/{issu template = "docs/changes/template.jinja" underlines = ["", "", ""] -[tool.changelog-bot] - -[tool.changelog-bot.towncrier_changelog] -enabled = true -verify_pr_number = true -changelog_skip_label = "no-changelog-entry-needed" - [[tool.towncrier.type]] directory = "feature" name = "Added" @@ -237,15 +245,3 @@ showcontent = true directory = "misc" name = "Internal" showcontent = true - -[tool.spin] -package = "mne_denoise" - -[tool.spin.commands] -"Development" = [ - ".spin/cmds.py:test", - ".spin/cmds.py:lint", - ".spin/cmds.py:docs", - ".spin/cmds.py:build", - ".spin/cmds.py:check", -] diff --git a/scripts/check_dist.py b/scripts/check_dist.py index 5e5c34a6..e89aaf8f 100644 --- a/scripts/check_dist.py +++ b/scripts/check_dist.py @@ -19,16 +19,12 @@ "viz": "matplotlib", } DEVELOPMENT_REQUIREMENTS = { - "build", "pytest", "sphinx", "ruff", - "prek", - "spin", - "yamllint", - "zizmor", - "codespell", - "pre-commit-hooks", + "pre-commit", + "mypy", + "build", "twine", "towncrier", } @@ -88,7 +84,7 @@ def _check_wheel(wheel: Path) -> Version: _, metadata = _wheel_metadata(archive) assert metadata["Name"] == "mne-denoise" - assert metadata["Requires-Python"] == ">=3.12" + assert metadata["Requires-Python"] == ">=3.11" assert metadata["License-Expression"] == "BSD-3-Clause" assert "LICENSE" in metadata.get_all("License-File", []) diff --git a/scripts/rename_towncrier.py b/scripts/rename_towncrier.py deleted file mode 100644 index f4d0eadd..00000000 --- a/scripts/rename_towncrier.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python3 -"""Rename an unnumbered Towncrier fragment to the current pull request.""" - -from __future__ import annotations - -import argparse -import json -import os -import sys -import tomllib -from pathlib import Path -from typing import Any - - -def _parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--pr-number", - type=int, - help="Pull request number; otherwise derive it from GITHUB_EVENT_PATH.", - ) - return parser - - -def _event_pr_number() -> int | None: - """Return the pull request number from a GitHub event, if applicable.""" - event_name = os.environ.get("GITHUB_EVENT_NAME", "") - event_path = os.environ.get("GITHUB_EVENT_PATH") - if not event_name.startswith("pull_request"): - print(f"No-op: {event_name or 'local execution'} is not a pull request.") - return None - if not event_path: - raise RuntimeError("GITHUB_EVENT_PATH is required for pull request events") - with Path(event_path).open(encoding="utf-8") as file: - event: dict[str, Any] = json.load(file) - number = event.get("number") or event.get("pull_request", {}).get("number") - if not isinstance(number, int) or number <= 0: - raise RuntimeError("could not find a positive pull request number in the event") - return number - - -def _towncrier_config() -> tuple[Path, tuple[str, ...]]: - """Return the configured fragment directory and supported fragment types.""" - with Path("pyproject.toml").open("rb") as file: - config = tomllib.load(file) - towncrier = config["tool"]["towncrier"] - directory = Path(towncrier["directory"]) - types = tuple(entry["directory"] for entry in towncrier["type"]) - if directory.is_absolute() or not types: - raise RuntimeError("Towncrier directory and types must be configured locally") - return directory, types - - -def rename(pr_number: int) -> int: - """Rename all supported unnumbered fragments and return a process status.""" - directory, types = _towncrier_config() - operations = [ - ( - directory / f"{fragment_type}.rst", - directory / f"{pr_number}.{fragment_type}.rst", - ) - for fragment_type in types - if (directory / f"{fragment_type}.rst").is_file() - ] - if not operations: - print(f"No unnumbered Towncrier fragments found in {directory}.") - return 0 - collisions = [target for _, target in operations if target.exists()] - if collisions: - names = ", ".join(str(path) for path in collisions) - print( - f"Refusing to overwrite existing Towncrier fragment(s): {names}", - file=sys.stderr, - ) - return 1 - for source, target in operations: - source.rename(target) - print(f"Renamed {source} -> {target}") - return 0 - - -def main() -> int: - """Run the Towncrier fragment renamer.""" - args = _parser().parse_args() - try: - pr_number = args.pr_number if args.pr_number is not None else _event_pr_number() - return 0 if pr_number is None else rename(pr_number) - except (OSError, KeyError, TypeError, ValueError, RuntimeError) as error: - print(f"Towncrier fragment renaming failed: {error}", file=sys.stderr) - return 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tests/test_rename_towncrier.py b/tests/test_rename_towncrier.py deleted file mode 100644 index df5b10b3..00000000 --- a/tests/test_rename_towncrier.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Tests for the local Towncrier fragment helper.""" - -from __future__ import annotations - -import subprocess -import sys -from pathlib import Path - -SCRIPT = Path(__file__).parents[1] / "scripts" / "rename_towncrier.py" -CONFIG = """ -[tool.towncrier] -directory = "docs/changes/devel/" - -[[tool.towncrier.type]] -directory = "feature" - -[[tool.towncrier.type]] -directory = "bugfix" - -[[tool.towncrier.type]] -directory = "doc" - -[[tool.towncrier.type]] -directory = "removal" - -[[tool.towncrier.type]] -directory = "misc" -""" - - -def _prepare(tmp_path: Path) -> None: - (tmp_path / "pyproject.toml").write_text(CONFIG, encoding="utf-8") - (tmp_path / "docs" / "changes" / "devel").mkdir(parents=True, exist_ok=True) - - -def _run(tmp_path: Path, *args: str) -> subprocess.CompletedProcess[str]: - _prepare(tmp_path) - return subprocess.run( - [sys.executable, str(SCRIPT), *args], - cwd=tmp_path, - capture_output=True, - text=True, - check=False, - ) - - -def test_rename_misc_fragment(tmp_path: Path) -> None: - _prepare(tmp_path) - fragment = tmp_path / "docs" / "changes" / "devel" / "misc.rst" - fragment.write_text("Tooling maintenance.\n", encoding="utf-8") - - result = _run(tmp_path, "--pr-number", "123") - - assert result.returncode == 0 - assert not fragment.exists() - assert (fragment.parent / "123.misc.rst").read_text(encoding="utf-8") == ( - "Tooling maintenance.\n" - ) - - -def test_rename_succeeds_without_fragments(tmp_path: Path) -> None: - result = _run(tmp_path, "--pr-number", "123") - - assert result.returncode == 0 - assert "No unnumbered" in result.stdout - - -def test_rename_refuses_to_overwrite(tmp_path: Path) -> None: - _prepare(tmp_path) - fragment_dir = tmp_path / "docs" / "changes" / "devel" - (fragment_dir / "misc.rst").write_text("new\n", encoding="utf-8") - (fragment_dir / "123.misc.rst").write_text("old\n", encoding="utf-8") - - result = _run(tmp_path, "--pr-number", "123") - - assert result.returncode == 1 - assert "Refusing to overwrite" in result.stderr - assert (fragment_dir / "misc.rst").exists() diff --git a/tools/pylock.ci-old.toml b/tools/pylock.ci-old.toml index 2fb84b0a..af299ca7 100644 --- a/tools/pylock.ci-old.toml +++ b/tools/pylock.ci-old.toml @@ -1,8 +1,8 @@ # This file was autogenerated by uv via the following command: -# uv pip compile pyproject.toml --python 3.12 --python-platform x86_64-unknown-linux-gnu --group test --group lockfile_extras --resolution lowest-direct --format pylock.toml --output-file tools/pylock.ci-old.toml --no-cache +# uv pip compile pyproject.toml --python 3.11 --python-platform x86_64-unknown-linux-gnu --group test --group lockfile_extras --resolution lowest-direct --format pylock.toml --output-file tools/pylock.ci-old.toml --no-cache lock-version = "1.0" created-by = "uv" -requires-python = ">=3.12" +requires-python = ">=3.11" [[packages]] name = "certifi" @@ -15,7 +15,7 @@ name = "charset-normalizer" version = "3.5.1" sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", upload-time = 2026-08-15T08:20:44Z, size = 171764, hashes = { sha256 = "6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3" } } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-08-15T08:17:17Z, size = 248801, hashes = { sha256 = "b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08" } }, + { url = "https://files.pythonhosted.org/packages/0d/35/731ac04aa0a097fc1c97f0994c375bdb230c6c96619db794208fe664e9ce/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-08-15T08:16:54Z, size = 262325, hashes = { sha256 = "c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8" } }, { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", upload-time = 2026-08-15T08:19:52Z, size = 253057, hashes = { sha256 = "a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99" } }, { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", upload-time = 2026-08-15T08:20:43Z, size = 68658, hashes = { sha256 = "6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6" } }, ] @@ -24,14 +24,14 @@ wheels = [ name = "contourpy" version = "1.3.3" sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", upload-time = 2025-07-26T12:03:12Z, size = 13466174, hashes = { sha256 = "083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2025-07-26T12:01:28Z, size = 362601, hashes = { sha256 = "4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1" } }] +wheels = [{ url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2025-07-26T12:01:10Z, size = 355238, hashes = { sha256 = "51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db" } }] [[packages]] name = "coverage" version = "7.16.0" sdist = { url = "https://files.pythonhosted.org/packages/d1/f5/deb1a27aa20746c0278ac998c4179e272004699b2d33959ce020c5ac1615/coverage-7.16.0.tar.gz", upload-time = 2026-08-28T21:54:37Z, size = 945620, hashes = { sha256 = "077f0964087883176ff6ab9b074694cae29f8c708273b13ca62c183c6ed716cd" } } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/29/dd89fd39af1a3b6e9a9c3eddeaf03f6376ba517d43d6cbf8b519177e2a10/coverage-7.16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", upload-time = 2026-08-28T21:51:33Z, size = 257790, hashes = { sha256 = "719a3feb6220dd32ed932d4c3676d17fb8739e2643b29c0e7c3af400ff80ac44" } }, + { url = "https://files.pythonhosted.org/packages/9a/27/ade10badacc00076854f0c5086fcf8975bb1a379d5288b587509e6ee9763/coverage-7.16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", upload-time = 2026-08-28T21:51:06Z, size = 255846, hashes = { sha256 = "7cae7715afa51dd7c9c42e6603bb46daf424c3449fdf06519cc658aa8d46e2e4" } }, { url = "https://files.pythonhosted.org/packages/b1/5a/234e8fadf85c3cc48cb31c247b9e8e0c7f06ece80f5b29f9b8c241f9da4c/coverage-7.16.0-py3-none-any.whl", upload-time = 2026-08-28T21:54:35Z, size = 214977, hashes = { sha256 = "245f7de6d023a5bba375dbec9f2e0869bfa26ac0cc639bbb7b4c814884000b73" } }, ] @@ -52,7 +52,7 @@ name = "fonttools" version = "4.63.0" sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", upload-time = 2026-05-14T12:04:30Z, size = 3597189, hashes = { sha256 = "caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0" } } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-05-14T12:03:20Z, size = 4999800, hashes = { sha256 = "58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8" } }, + { url = "https://files.pythonhosted.org/packages/0b/43/a81f20050a3115b57d62c8e781446949512eac36690dc384ccea65ff4cc1/fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-05-14T12:03:03Z, size = 5082308, hashes = { sha256 = "d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18" } }, { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", upload-time = 2026-05-14T12:04:29Z, size = 1164562, hashes = { sha256 = "445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d" } }, ] @@ -76,15 +76,15 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f [[packages]] name = "joblib" -version = "1.4.0" -sdist = { url = "https://files.pythonhosted.org/packages/7c/c3/94c9e4886e8f33832690ab48fdac4a121a7bfec3e2c044c9f2762aa9068e/joblib-1.4.0.tar.gz", upload-time = 2024-04-08T15:08:19Z, size = 2115863, hashes = { sha256 = "1eb0dc091919cd384490de890cb5dfd538410a6d4b3b54eef09fb8c50b409b1c" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/ae/e2/4dea6313ef2b38442fccbbaf4017e50a6c3c8a50e8ee9b512783e5c90409/joblib-1.4.0-py3-none-any.whl", upload-time = 2024-04-08T15:08:14Z, size = 301185, hashes = { sha256 = "42942470d4062537be4d54c83511186da1fc14ba354961a2114da91efa9a4ed7" } }] +version = "1.2.0" +sdist = { url = "https://files.pythonhosted.org/packages/45/dd/a5435a6902d6315241c48a5343e6e6675b007e05d3738ed97a7a47864e53/joblib-1.2.0.tar.gz", upload-time = 2022-09-16T10:01:07Z, size = 313200, hashes = { sha256 = "e1cee4a79e4af22881164f218d4311f60074197fb707e082e803b61f6d137018" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/91/d4/3b4c8e5a30604df4c7518c562d4bf0502f2fa29221459226e140cf846512/joblib-1.2.0-py3-none-any.whl", upload-time = 2022-09-16T10:01:04Z, size = 297969, hashes = { sha256 = "091138ed78f800342968c523bdde947e7a305b8594b910a0fea2ab83c3c6d385" } }] [[packages]] name = "kiwisolver" version = "1.5.1" sdist = { url = "https://files.pythonhosted.org/packages/ba/07/bd78e6a8fae171ea041ef5bba3ed21a003522fa088834b069b1909981f30/kiwisolver-1.5.1.tar.gz", upload-time = 2026-08-28T10:28:27Z, size = 104395, hashes = { sha256 = "f1303ef2eec81262a4b708c3e858afe58d7c75ad91c1c05266eda7673369859a" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/fc/f4/dadfec469313c7f428efa7e84b4aba9732f813c13ea7131a24b7b008ef57/kiwisolver-1.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-08-28T10:25:31Z, size = 1477929, hashes = { sha256 = "34633ecf50d16187ab8e5528b7a2530f2feb4e23f300db4672538b51cfc5cd38" } }] +wheels = [{ url = "https://files.pythonhosted.org/packages/89/00/05c2d0369ac322d22d5c05f84b5c4a6856fa6207fbae42869108a28f0383/kiwisolver-1.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-08-28T10:25:08Z, size = 1438206, hashes = { sha256 = "95a02752aa032eef4aed01cda6d9b687c669bd0396bf4519eef8bba22a286720" } }] [[packages]] name = "lazy-loader" @@ -96,13 +96,13 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da168 name = "markupsafe" version = "3.0.3" sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", upload-time = 2025-09-27T18:37:40Z, size = 80313, hashes = { sha256 = "722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2025-09-27T18:36:33Z, size = 22947, hashes = { sha256 = "d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d" } }] +wheels = [{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2025-09-27T18:36:22Z, size = 22940, hashes = { sha256 = "0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf" } }] [[packages]] name = "matplotlib" version = "3.8.0" sdist = { url = "https://files.pythonhosted.org/packages/23/e1/77016194621fb1356aafeb2186f07b5dede62ea2043bf03f82325c4fccc5/matplotlib-3.8.0.tar.gz", upload-time = 2023-09-15T04:49:03Z, size = 35864435, hashes = { sha256 = "df8505e1c19d5c2c26aff3497a7cbd3ccfc2e97043d1e4db3e76afa399164b69" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/77/cd/1464efc9fe354026b8d2fb4ebb2f1746559b0e38308104d3ee60a5a05c71/matplotlib-3.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2023-09-15T04:51:54Z, size = 11602343, hashes = { sha256 = "dae97fdd6996b3a25da8ee43e3fc734fff502f396801063c6b76c20b56683196" } }] +wheels = [{ url = "https://files.pythonhosted.org/packages/65/5b/3b8fd7d66043f0638a35fa650570cbe69efd42fe169e5024f9307598b47e/matplotlib-3.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2023-09-15T04:51:00Z, size = 11615276, hashes = { sha256 = "eee482731c8c17d86d9ddb5194d38621f9b0f0d53c99006275a12523ab021732" } }] [[packages]] name = "mne" @@ -114,7 +114,7 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/6c/da/a3280dbd8f0024b name = "numpy" version = "1.26.0" sdist = { url = "https://files.pythonhosted.org/packages/55/b3/b13bce39ba82b7398c06d10446f5ffd5c07db39b09bd37370dc720c7951c/numpy-1.26.0.tar.gz", upload-time = 2023-09-16T20:12:58Z, size = 15633455, hashes = { sha256 = "f93fc78fe8bf15afe2b8d6b6499f1c73953169fad1e9a8dd086cdff3190e7fdf" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/e3/e2/4ecfbc4a2e3f9d227b008c92a5d1f0370190a639b24fec3b226841eaaf19/numpy-1.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2023-09-16T20:05:55Z, size = 17883864, hashes = { sha256 = "7f6bad22a791226d0a5c7c27a80a20e11cfe09ad5ef9084d4d3fc4a299cca505" } }] +wheels = [{ url = "https://files.pythonhosted.org/packages/c4/36/161e2f8110f8c49e59f6107bd6da4257d30aff9f06373d0471811f73dcc5/numpy-1.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2023-09-16T20:02:49Z, size = 18178118, hashes = { sha256 = "e062aa24638bb5018b7841977c360d2f5917268d125c833a686b7cbabbec496c" } }] [[packages]] name = "packaging" @@ -124,15 +124,15 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9ead [[packages]] name = "pandas" -version = "2.1.1" -sdist = { url = "https://files.pythonhosted.org/packages/3d/0e/2c225d7a5de6ca0ec7d729aff6ef560544596f3a9bfed77f6dbc1713dbb5/pandas-2.1.1.tar.gz", upload-time = 2023-09-20T21:05:19Z, size = 4266250, hashes = { sha256 = "fecb198dc389429be557cde50a2d46da8434a17fe37d7d41ff102e3987fd947b" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/41/db/fc107df31c06976764e753074cc71cbe1c7062481f668746f8d498cafcb6/pandas-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2023-09-20T21:04:48Z, size = 11652903, hashes = { sha256 = "29deb61de5a8a93bdd033df328441a79fcf8dd3c12d5ed0b41a395eef9cd76f0" } }] +version = "1.5.0" +sdist = { url = "https://files.pythonhosted.org/packages/2a/24/f5042daa59b91e94e6ea41edbb28d2b7e3712d0cf54a76f9ffde394efbe7/pandas-1.5.0.tar.gz", upload-time = 2022-09-19T15:55:13Z, size = 5191537, hashes = { sha256 = "3ee61b881d2f64dd90c356eb4a4a4de75376586cd3c9341c6c0fcaae18d52977" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/fa/fe/c81ad3991f2c6aeacf01973f1d37b1dc76c0682f312f104741602a9557f1/pandas-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2022-09-19T15:53:58Z, size = 12022642, hashes = { sha256 = "e252a9e49b233ff96e2815c67c29702ac3a062098d80a170c506dff3470fd060" } }] [[packages]] name = "pillow" version = "12.3.0" sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", upload-time = 2026-07-01T11:56:38Z, size = 47025035, hashes = { sha256 = "3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-07-01T11:54:13Z, size = 6940830, hashes = { sha256 = "78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91" } }] +wheels = [{ url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-07-01T11:53:53Z, size = 6934408, hashes = { sha256 = "23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd" } }] [[packages]] name = "platformdirs" @@ -198,19 +198,19 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e name = "scikit-learn" version = "1.5.0" sdist = { url = "https://files.pythonhosted.org/packages/bf/8a/06e499bca463905000f50e461c9445e949aafdd33ea3b62024aa2238b83d/scikit_learn-1.5.0.tar.gz", upload-time = 2024-05-21T16:34:07Z, size = 7820839, hashes = { sha256 = "789e3db01c750ed6d496fa2db7d50637857b451e57bcae863bff707c1247bef7" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/ae/54/e70102a9c12d27d985ba659f336851732415e5a02864bef2ead36afaf15d/scikit_learn-1.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-05-21T16:33:45Z, size = 13065320, hashes = { sha256 = "a3a10e1d9e834e84d05e468ec501a356226338778769317ee0b84043c0d8fb06" } }] +wheels = [{ url = "https://files.pythonhosted.org/packages/46/c0/63d3a8da39a2ee051df229111aa93f6dca2b56f8080abd34993938166455/scikit_learn-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-05-21T16:33:29Z, size = 13328661, hashes = { sha256 = "118a8d229a41158c9f90093e46b3737120a165181a1b58c03461447aa4657415" } }] [[packages]] name = "scipy" version = "1.13.0" sdist = { url = "https://files.pythonhosted.org/packages/fb/a3/328965862f41ba67d27ddd26205962007ec87d99eec6d364a29bf00ac093/scipy-1.13.0.tar.gz", upload-time = 2024-04-02T21:48:22Z, size = 57204550, hashes = { sha256 = "58569af537ea29d3f78e5abd18398459f195546bb3be23d16677fb26616cc11e" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/87/8c/97e545034c94d0bbbc3af3202551c3d6020e5f8d2ee37ebcabd9a2048174/scipy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-04-02T21:43:02Z, size = 38213102, hashes = { sha256 = "1e7626dfd91cdea5714f343ce1176b6c4745155d234f1033584154f60ef1ff42" } }] +wheels = [{ url = "https://files.pythonhosted.org/packages/e8/fb/e5955e2ddbdf2baee461eb53ec8d0adedd20a6dfc5510ef8d5e7e44ba461/scipy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2024-04-02T21:42:22Z, size = 38562576, hashes = { sha256 = "9ff7dad5d24a8045d836671e082a490848e8639cabb3dbdacb29f943a678683d" } }] [[packages]] name = "seaborn" -version = "0.13.0" -sdist = { url = "https://files.pythonhosted.org/packages/06/6f/caf0741c5787358b0efba3b4db7f8235e3a48e719ad2444bbd51485f966c/seaborn-0.13.0.tar.gz", upload-time = 2023-09-29T18:58:36Z, size = 1455480, hashes = { sha256 = "0e76abd2ec291c655b516703c6a022f0fd5afed26c8e714e8baef48150f73598" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/7b/e5/83fcd7e9db036c179e0352bfcd20f81d728197a16f883e7b90307a88e65e/seaborn-0.13.0-py3-none-any.whl", upload-time = 2023-09-29T18:58:33Z, size = 294583, hashes = { sha256 = "70d740828c48de0f402bb17234e475eda687e3c65f4383ea25d0cc4728f7772e" } }] +version = "0.12.0" +sdist = { url = "https://files.pythonhosted.org/packages/0a/87/9d713b302b7319f58e76f37fb2308377035c594b76bd50fe3696dad3a27e/seaborn-0.12.0.tar.gz", upload-time = 2022-09-06T03:04:03Z, size = 1407601, hashes = { sha256 = "893f17292d8baca616c1578ddb58eb25c72d622f54fc5ee329c8207dc9b57b23" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/c2/03/14991c1f18422eb640f6fe6eadf9a675bb21d9339236d64c3d12bd0eb1a4/seaborn-0.12.0-py3-none-any.whl", upload-time = 2022-09-06T03:04:01Z, size = 285116, hashes = { sha256 = "cbeff3deef7c2515aa0af99b2c7e02dc5bf8b42c936a74d8e4b416905b549db0" } }] [[packages]] name = "six" @@ -224,18 +224,22 @@ version = "3.6.0" sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", upload-time = 2025-03-13T13:49:23Z, size = 21274, hashes = { sha256 = "8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e" } } wheels = [{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", upload-time = 2025-03-13T13:49:21Z, size = 18638, hashes = { sha256 = "43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb" } }] +[[packages]] +name = "tomli" +version = "2.4.1" +marker = "python_full_version <= '3.11'" +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", upload-time = 2026-03-25T20:22:03Z, size = 17543, hashes = { sha256 = "7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f" } } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-03-25T20:21:14Z, size = 243824, hashes = { sha256 = "5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9" } }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", upload-time = 2026-03-25T20:22:03Z, size = 14583, hashes = { sha256 = "0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe" } }, +] + [[packages]] name = "tqdm" version = "4.66.0" sdist = { url = "https://files.pythonhosted.org/packages/f1/fb/6f40278d3b74f1486147aebf828ad081226f4f80b5b31a042386acc76dde/tqdm-4.66.0.tar.gz", upload-time = 2023-08-09T10:45:13Z, size = 169029, hashes = { sha256 = "cc6e7e52202d894e66632c5c8a9330bd0e3ff35d2965c93ca832114a3d865362" } } wheels = [{ url = "https://files.pythonhosted.org/packages/a5/d6/502a859bac4ad5e274255576cd3e15ca273cdb91731bc39fb840dd422ee9/tqdm-4.66.0-py3-none-any.whl", upload-time = 2023-08-09T10:45:10Z, size = 78149, hashes = { sha256 = "39d459c7140b7890174e69d4d68d6291bc774a55b4bc5d93c0b760798ac5a03e" } }] -[[packages]] -name = "tzdata" -version = "2026.3" -sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", upload-time = 2026-07-10T08:50:37Z, size = 198674, hashes = { sha256 = "4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", upload-time = 2026-07-10T08:50:36Z, size = 348168, hashes = { sha256 = "dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931" } }] - [[packages]] name = "urllib3" version = "2.7.0" From f11ce7fca8705e0dce2740a3ea9c4d5bdf3bece0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 30 Aug 2026 23:07:23 +0000 Subject: [PATCH 11/11] chore(deps-dev): update pre-commit requirement from >=3.6.0 to >=4.6.2 Updates the requirements on [pre-commit](https://github.com/pre-commit/pre-commit) to permit the latest version. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.6.0...v4.6.2) --- updated-dependencies: - dependency-name: pre-commit dependency-version: 4.6.2 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d3ffc9bb..a951a2f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,7 +104,7 @@ build = [ lint = [ "ruff>=0.16.0", - "pre-commit>=3.6.0", + "pre-commit>=4.6.2", ] typecheck = [