diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 951ae12d839..b3f5191c438 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -63,6 +63,21 @@ repos: - id: toml-sort-fix files: pyproject.toml + # dependencies + - repo: local + hooks: + - id: dependency-sync + name: Sync dependency list between pyproject.toml and README.rst + language: python + entry: ./tools/hooks/sync_dependencies.py + files: pyproject.toml + additional_dependencies: ["mne"] + + +# these should *not* be run on CIs: +ci: + skip: [dependency-sync] # needs MNE to work, which exceeds the free tier space alloc. + # The following are too slow to run on local commits, so let's only run on CIs: # # - repo: https://github.com/pre-commit/mirrors-mypy diff --git a/README.rst b/README.rst index 745e74849c3..50e0daaa52c 100644 --- a/README.rst +++ b/README.rst @@ -43,9 +43,6 @@ only, use pip_ in a terminal: $ pip install --upgrade mne -The current MNE-Python release requires Python 3.9 or higher. MNE-Python 0.17 -was the last release to support Python 2.7. - For more complete instructions, including our standalone installers and more advanced installation methods, please refer to the `installation guide`_. @@ -73,42 +70,20 @@ Dependencies The minimum required dependencies to run MNE-Python are: +.. ↓↓↓ BEGIN CORE DEPS LIST. DO NOT EDIT! HANDLED BY PRE-COMMIT HOOK ↓↓↓ + - `Python `__ ≥ 3.9 -- `NumPy `__ ≥ 1.24 -- `SciPy `__ ≥ 1.10 +- `NumPy `__ ≥ 1.23 +- `SciPy `__ ≥ 1.9 - `Matplotlib `__ ≥ 3.6 - `Pooch `__ ≥ 1.5 - `tqdm `__ - `Jinja2 `__ - `decorator `__ -- `lazy_loader `__ - -For full functionality, some functions require: - -- `scikit-learn `__ ≥ 1.2 -- `Joblib `__ ≥ 1.2 (for parallelization) -- `mne-qt-browser `__ ≥ 0.5 (for fast raw data visualization) -- `Qt `__ ≥ 5.15 via one of the following bindings (for fast raw data visualization and interactive 3D visualization): - - - `PySide6 `__ ≥ 6.0 - - `PyQt6 `__ ≥ 6.0 - - `PyQt5 `__ ≥ 5.15 - -- `Numba `__ ≥ 0.56.4 -- `NiBabel `__ ≥ 3.2.1 -- `OpenMEEG `__ ≥ 2.5.6 -- `pandas `__ ≥ 1.5.2 -- `Picard `__ ≥ 0.3 -- `CuPy `__ ≥ 9.0.0 (for NVIDIA CUDA acceleration) -- `DIPY `__ ≥ 1.4.0 -- `imageio `__ ≥ 2.8.0 -- `PyVista `__ ≥ 0.37 (for 3D visualization) -- `PyVistaQt `__ ≥ 0.9 (for 3D visualization) -- `mffpy `__ ≥ 0.5.7 -- `h5py `__ -- `h5io `__ -- `pymatreader `__ +- `lazy-loader `__ ≥ 0.3 +- `packaging `__ +.. ↑↑↑ END CORE DEPS LIST. DO NOT EDIT! HANDLED BY PRE-COMMIT HOOK ↑↑↑ Contributing ^^^^^^^^^^^^ diff --git a/pyproject.toml b/pyproject.toml index d58793a6e45..9bdf8244b99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ scripts = {mne = "mne.commands.utils:main"} [project.optional-dependencies] # Leave this one here for backward-compat data = [] -dev = ["mne[test,doc]", "rcssmin"] +dev = ["mne[doc,test]", "rcssmin"] # Dependencies for building the documentation doc = [ "graphviz", diff --git a/tools/hooks/sync_dependencies.py b/tools/hooks/sync_dependencies.py new file mode 100755 index 00000000000..0878a5f56eb --- /dev/null +++ b/tools/hooks/sync_dependencies.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python + +# Authors: The MNE-Python contributors. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. + +import re +from importlib.metadata import metadata +from pathlib import Path + +from mne.utils import _pl, warn + +README_PATH = Path(__file__).parents[2] / "README.rst" +BEGIN = ".. ↓↓↓ BEGIN CORE DEPS LIST. DO NOT EDIT! HANDLED BY PRE-COMMIT HOOK ↓↓↓" +END = ".. ↑↑↑ END CORE DEPS LIST. DO NOT EDIT! HANDLED BY PRE-COMMIT HOOK ↑↑↑" + +CORE_DEPS_URLS = { + "Python": "https://www.python.org", + "NumPy": "https://numpy.org", + "SciPy": "https://scipy.org", + "Matplotlib": "https://matplotlib.org", + "Pooch": "https://www.fatiando.org/pooch/latest/", + "tqdm": "https://tqdm.github.io", + "Jinja2": "https://palletsprojects.com/p/jinja/", + "decorator": "https://github.com/micheles/decorator", + "lazy-loader": "https://pypi.org/project/lazy_loader", + "packaging": "https://packaging.pypa.io/en/stable/", +} + + +def _prettify_pin(pin): + if pin is None: + return "" + pins = pin.split(",") + replacements = { + "<=": " ≤ ", + ">=": " ≥ ", + "<": " < ", + ">": " > ", + } + for old, new in replacements.items(): + pins = [p.replace(old, new) for p in pins] + pins = reversed(pins) + return ",".join(pins) + + +# get the dependency info +py_pin = metadata("mne").get("Requires-Python") +all_deps = metadata("mne").get_all("Requires-Dist") +core_deps = [f"python{py_pin}", *[dep for dep in all_deps if "extra ==" not in dep]] +pattern = re.compile(r"(?P[A-Za-z_\-\d]+)(?P[<>=]+.*)?") +core_deps_pins = { + dep["name"]: _prettify_pin(dep["pin"]) for dep in map(pattern.match, core_deps) +} +# don't show upper pin on NumPy (not important for users, just devs) +new_pin = core_deps_pins["numpy"].split(",") +new_pin.remove(" < 3") +core_deps_pins["numpy"] = new_pin[0] + +# make sure our URLs dict is minimal and complete +missing_urls = set(core_deps_pins) - {dep.lower() for dep in CORE_DEPS_URLS} +extra_urls = {dep.lower() for dep in CORE_DEPS_URLS} - set(core_deps_pins) +update_msg = ( + "please update `CORE_DEPS_URLS` mapping in `tools/hooks/sync_dependencies.py`." +) +if missing_urls: + _s = _pl(missing_urls) + raise RuntimeError( + f"Missing URL{_s} for package{_s} {', '.join(missing_urls)}; {update_msg}" + ) +if extra_urls: + _s = _pl(extra_urls) + warn(f"Superfluous URL{_s} for package{_s} {', '.join(extra_urls)}; {update_msg}") + +# construct the rST +core_deps_bullets = [ + f"- `{key} <{url}>`__{core_deps_pins[key.lower()]}" + for key, url in CORE_DEPS_URLS.items() +] +core_deps_rst = "\n" + "\n".join(core_deps_bullets) + "\n" + +# rewrite the README file +lines = README_PATH.read_text("utf-8").splitlines() +out_lines = list() +skip = False +for line in lines: + if line.strip() == BEGIN: + skip = True + out_lines.append(line) + out_lines.append(core_deps_rst) + if line.strip() == END: + skip = False + if not skip: + out_lines.append(line) +README_PATH.write_text("\n".join(out_lines) + "\n", encoding="utf-8")