From fef045bdc2bd6eef85fcbdaa74e33ee4d3b831ec Mon Sep 17 00:00:00 2001 From: Enning Yang Date: Mon, 4 May 2026 10:16:18 -0400 Subject: [PATCH 1/5] Fix CI and assorted bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI: - python_unittests: bump checkout to v4 and setup-python to v5; install setuptools/wheel explicitly before and after the editable install so abagen's `pkg_resources` import does not break test collection on Python 3.12 (where setuptools is no longer in the venv by default). - MATLAB_unittests: bump checkout to v4, setup-python to v5, setup-matlab to v2 and switch to run-command v2 so we can call pyenv() before runtests; without it test_precomputed/load_pkl errors with MATLAB:Python:PythonUnavailable on the GitHub runners. Drop the unsupported R019b matrix entry (setup-matlab v2 requires R2020a+); test against R2022a (Python 3.9) and latest (Python 3.10) instead. Bugs: - SLM.py: import sqrt from math, not cmath (cmath returns complex values; the code uses real sqrt only). - SLM.py: drop the duplicate fetch_template_surface import — it is re-exported from brainstat.datasets and was being imported twice. - _t_test.py: replace sys.exit("Contrast is not estimable") with raise ValueError. Library code should never terminate the host process; remove the now-unused `import sys`. --- .github/workflows/MATLAB_unittests.yml | 45 +++++++++++++++++++------- .github/workflows/python_unittests.yml | 16 ++++++--- brainstat/stats/SLM.py | 4 +-- brainstat/stats/_t_test.py | 3 +- 4 files changed, 48 insertions(+), 20 deletions(-) diff --git a/.github/workflows/MATLAB_unittests.yml b/.github/workflows/MATLAB_unittests.yml index f3c20bf1..429836a7 100644 --- a/.github/workflows/MATLAB_unittests.yml +++ b/.github/workflows/MATLAB_unittests.yml @@ -7,37 +7,60 @@ on: paths: - brainstat_matlab/* - '**.m' + - .github/workflows/MATLAB_unittests.yml pull_request: paths: - brainstat_matlab/* - '**.m' + - .github/workflows/MATLAB_unittests.yml jobs: matlab_unit_test: strategy: fail-fast: false matrix: - matlab-version: [R019b, latest] + include: + - matlab-version: R2022a + python-version: '3.9' + - matlab-version: latest + python-version: '3.10' runs-on: ubuntu-latest - + steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + id: setup-python + uses: actions/setup-python@v5 with: - python-version: 3.8 - - name: Install dependencies + python-version: ${{ matrix.python-version }} + + - name: Install Python dependencies for MATLAB tests shell: bash run: | + python -m pip install --upgrade pip setuptools wheel + # numpy + vtk are required by recursive_pkl_conversion in + # tests/@test_precomputed/test_precomputed.m, which loads pickle + # fixtures via MATLAB's Python bridge. python -m pip install numpy vtk [[ -z $(git remote show origin | grep "Fetch URL:" | grep git@github.com:MICA-MNI/BrainStat.git) ]] && git config remote.upstream.fetch refs/heads/*:refs/remotes/upstream/* || git config remote.origin.fetch refs/heads/*:refs/origin/upstream/* git fetch origin test-data-2.0 git checkout origin/test-data-2.0 -- extern/test-data git reset extern/test-data + - name: Install MATLAB - uses: matlab-actions/setup-matlab@v0 - - name: Run tests - uses: matlab-actions/run-tests@v0 + uses: matlab-actions/setup-matlab@v2 + with: + release: ${{ matrix.matlab-version }} + + - name: Run tests + uses: matlab-actions/run-command@v2 with: - source-folder: brainstat_matlab + # Configure MATLAB's Python interpreter explicitly before running the + # tests; otherwise test_precomputed/load_pkl errors with + # MATLAB:Python:PythonUnavailable on the GitHub runners. + command: | + pyenv('Version', '${{ steps.setup-python.outputs.python-path }}'); + results = runtests('brainstat_matlab', 'IncludeSubfolders', true); + if any([results.Failed]); error('%d MATLAB test(s) failed.', sum([results.Failed])); end diff --git a/.github/workflows/python_unittests.yml b/.github/workflows/python_unittests.yml index 76b6496f..1e45d7c0 100644 --- a/.github/workflows/python_unittests.yml +++ b/.github/workflows/python_unittests.yml @@ -16,19 +16,25 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - + - name: Install Dependencies shell: bash - run: | - python -m pip install --upgrade pip setuptools + run: | + # setuptools is no longer bundled by default in Python 3.12 venvs, but + # transitive deps (e.g. abagen) still import pkg_resources. Install it + # explicitly (both before and after the editable install) so test + # collection does not fail with ModuleNotFoundError: pkg_resources. + python -m pip install --upgrade pip + python -m pip install --upgrade setuptools wheel [[ -z $(git remote show origin | grep "Fetch URL:" | grep git@github.com:MICA-MNI/BrainStat.git) ]] && git config remote.upstream.fetch refs/heads/*:refs/remotes/upstream/* || git config remote.origin.fetch refs/heads/*:refs/origin/upstream/* git fetch origin test-data-2.0 python -m pip install -e .[dev] + python -m pip install --upgrade setuptools - name: Test with pytest shell: bash diff --git a/brainstat/stats/SLM.py b/brainstat/stats/SLM.py index 842f37a2..c6dfa520 100644 --- a/brainstat/stats/SLM.py +++ b/brainstat/stats/SLM.py @@ -1,7 +1,7 @@ # type: ignore """ Standard Linear regression models. """ import warnings -from cmath import sqrt +from math import sqrt from pathlib import Path from pprint import pformat from typing import Optional, Sequence, Tuple, Union @@ -17,7 +17,7 @@ from brainstat._typing import ArrayLike from brainstat.datasets import fetch_parcellation, fetch_template_surface -from brainstat.datasets.base import fetch_template_surface, fetch_yeo_networks_metadata +from brainstat.datasets.base import fetch_yeo_networks_metadata from brainstat.mesh.utils import _mask_edges, mesh_edges from brainstat.stats.terms import FixedEffect, MixedEffect from brainstat.stats.utils import apply_mask, undo_mask diff --git a/brainstat/stats/_t_test.py b/brainstat/stats/_t_test.py index d7deda46..9f7ba3a2 100644 --- a/brainstat/stats/_t_test.py +++ b/brainstat/stats/_t_test.py @@ -1,5 +1,4 @@ import math -import sys import warnings import numpy as np @@ -39,7 +38,7 @@ def _t_test(self) -> None: if np.square(np.dot(null_space(self.X).T, c)).sum() / np.square( c ).sum() > np.spacing(1): - sys.exit("Contrast is not estimable :-(") + raise ValueError("Contrast is not estimable.") else: c = np.dot(pinvX, self.contrast) From 186ccfe3f5b4d8458096c060244c4a9de2371798 Mon Sep 17 00:00:00 2001 From: Enning Yang Date: Mon, 4 May 2026 10:23:02 -0400 Subject: [PATCH 2/5] CI: pin setuptools<81 and addpath brainstat_matlab in MATLAB tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups after first CI run on this branch: - Python jobs were still failing with ModuleNotFoundError: pkg_resources. setuptools 82.0.1 was being installed, but setuptools 81+ no longer exposes the legacy pkg_resources module on import. abagen 0.1.3 (resolved from `abagen>=0.1` in requirements.txt) still does `from pkg_resources import resource_filename` at import time, so test collection of brainstat/tests/test_genetics.py blows up. Pin setuptools<81 in CI; force-reinstall after the editable install in case it gets bumped back. Real fix is upstream — when abagen drops pkg_resources, this pin can go. - MATLAB jobs were failing with `Undefined function fetch_template_surface` (and friends): switching from run-tests@v2 to run-command@v2 lost the implicit `addpath(source-folder)` that run-tests does. Add an explicit `addpath(genpath('brainstat_matlab'))` before runtests. --- .github/workflows/MATLAB_unittests.yml | 5 ++++- .github/workflows/python_unittests.yml | 15 +++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/MATLAB_unittests.yml b/.github/workflows/MATLAB_unittests.yml index 429836a7..89b629be 100644 --- a/.github/workflows/MATLAB_unittests.yml +++ b/.github/workflows/MATLAB_unittests.yml @@ -59,8 +59,11 @@ jobs: with: # Configure MATLAB's Python interpreter explicitly before running the # tests; otherwise test_precomputed/load_pkl errors with - # MATLAB:Python:PythonUnavailable on the GitHub runners. + # MATLAB:Python:PythonUnavailable on the GitHub runners. Also need to + # add brainstat_matlab to the MATLAB path explicitly — run-tests v2 + # used to do this via source-folder, but run-command does not. command: | + addpath(genpath('brainstat_matlab')); pyenv('Version', '${{ steps.setup-python.outputs.python-path }}'); results = runtests('brainstat_matlab', 'IncludeSubfolders', true); if any([results.Failed]); error('%d MATLAB test(s) failed.', sum([results.Failed])); end diff --git a/.github/workflows/python_unittests.yml b/.github/workflows/python_unittests.yml index 1e45d7c0..3fe8d8e4 100644 --- a/.github/workflows/python_unittests.yml +++ b/.github/workflows/python_unittests.yml @@ -25,16 +25,19 @@ jobs: - name: Install Dependencies shell: bash run: | - # setuptools is no longer bundled by default in Python 3.12 venvs, but - # transitive deps (e.g. abagen) still import pkg_resources. Install it - # explicitly (both before and after the editable install) so test - # collection does not fail with ModuleNotFoundError: pkg_resources. + # setuptools 81+ no longer ships the legacy `pkg_resources` module on + # import, but transitive deps (e.g. abagen 0.1.3 in + # brainstat/context/genetics.py) still rely on it. Pin <81 so test + # collection succeeds; revisit when abagen drops the pkg_resources + # import. python -m pip install --upgrade pip - python -m pip install --upgrade setuptools wheel + python -m pip install 'setuptools<81' wheel [[ -z $(git remote show origin | grep "Fetch URL:" | grep git@github.com:MICA-MNI/BrainStat.git) ]] && git config remote.upstream.fetch refs/heads/*:refs/remotes/upstream/* || git config remote.origin.fetch refs/heads/*:refs/origin/upstream/* git fetch origin test-data-2.0 python -m pip install -e .[dev] - python -m pip install --upgrade setuptools + # Reinstall pinned setuptools in case the editable install pulled it + # back up to 81+. + python -m pip install --force-reinstall 'setuptools<81' - name: Test with pytest shell: bash From e092dd2fa50fb4ab15a1678b5bd638d2fc9b7369 Mon Sep 17 00:00:00 2001 From: Enning Yang Date: Mon, 4 May 2026 10:36:23 -0400 Subject: [PATCH 3/5] CI: handle numpy 2.x scalar conversion and pin pandas <2.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the previous CI fix, jobs on Python 3.11/3.12 surface two more upstream-version regressions: - _t_test.py:175 — `float(np.sqrt(... / vf))` raises TypeError on numpy >=2.0 when the inner expression is a size-1 ndarray (numpy 2 dropped the implicit 1d→scalar conversion). Switch to `.item()`, which returns a native Python scalar in both numpy 1.x and 2.x. - abagen 0.1.3 calls `DataFrame.groupby(sid, axis=1)` in `abagen/probes_.py`, which raises in pandas 2.1+ where the `axis` keyword was removed. Until abagen ships a patched release, pin `pandas<2.1` in both setup.py install_requires and requirements.txt. --- brainstat/stats/_t_test.py | 4 +++- requirements.txt | 2 +- setup.py | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/brainstat/stats/_t_test.py b/brainstat/stats/_t_test.py index 9f7ba3a2..7cbf07d0 100644 --- a/brainstat/stats/_t_test.py +++ b/brainstat/stats/_t_test.py @@ -172,6 +172,8 @@ def _t_test(self) -> None: M[M_ef] = ef_duplicate[:, i] M[M_sse] = sse_matrix.flatten() - self.t[i] = float(np.sqrt(-np.linalg.det(M) / det_sse / vf)) + # Use .item() rather than float(): numpy >=2.0 raises + # TypeError when float() is applied to size-1 ndarrays. + self.t[i] = np.sqrt(-np.linalg.det(M) / det_sse / vf).item() self.t = np.atleast_2d(self.t) diff --git a/requirements.txt b/requirements.txt index e7f3b071..acb2dcff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ nibabel nilearn>=0.7.0 nimare numpy>=1.21 -pandas>=1.3 +pandas>=1.3,<2.1 # abagen 0.1.3 still calls DataFrame.groupby(axis=...), removed in pandas 2.1 scikit_learn>=1.0 scipy>=1.7 statsmodels diff --git a/setup.py b/setup.py index 62c83eda..39137540 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,9 @@ "nilearn>=0.7.0", "nimare", "numpy>=1.21", - "pandas>=1.3", + # pandas 2.1 removed DataFrame.groupby(axis=...), which abagen 0.1.3 + # still uses; pin <2.1 until upstream is patched. + "pandas>=1.3,<2.1", "scikit_learn>=1.0", "scipy>=1.7", "templateflow", From 902cc1a31674100864529b03c4f91353d15a15d7 Mon Sep 17 00:00:00 2001 From: Enning Yang Date: Mon, 4 May 2026 10:45:49 -0400 Subject: [PATCH 4/5] CI: also pin numpy<2 alongside pandas<2.1 pandas<2.1 resolves to 2.0.x, whose C extensions were built against the numpy 1.x ABI. Installing it alongside numpy 2.x produces `ValueError: numpy.dtype size changed, may indicate binary incompatibility. Expected 96 from C header, got 88 from PyObject` at import time, killing test collection across every matrix entry. Pin numpy<2 so the entire stack stays on numpy 1.x ABI until abagen is patched and the pandas pin can be lifted. --- requirements.txt | 8 ++++++-- setup.py | 7 ++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index acb2dcff..fca19be6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,8 +7,12 @@ neurosynth nibabel nilearn>=0.7.0 nimare -numpy>=1.21 -pandas>=1.3,<2.1 # abagen 0.1.3 still calls DataFrame.groupby(axis=...), removed in pandas 2.1 +# numpy and pandas are pinned together: pandas<2.1 is needed because abagen +# 0.1.3 still calls DataFrame.groupby(axis=...) which pandas 2.1 removed; the +# resulting pandas 2.0.x is built against numpy 1.x ABI, so numpy must also +# be <2 to avoid `numpy.dtype size changed` runtime errors. +numpy>=1.21,<2 +pandas>=1.3,<2.1 scikit_learn>=1.0 scipy>=1.7 statsmodels diff --git a/setup.py b/setup.py index 39137540..ebd5d201 100755 --- a/setup.py +++ b/setup.py @@ -44,9 +44,10 @@ "nibabel", "nilearn>=0.7.0", "nimare", - "numpy>=1.21", - # pandas 2.1 removed DataFrame.groupby(axis=...), which abagen 0.1.3 - # still uses; pin <2.1 until upstream is patched. + # numpy<2 is required so the pinned pandas<2.1 (whose 2.0.x C + # extensions were built against the numpy 1.x ABI) loads cleanly. + # See requirements.txt for the abagen-driven rationale. + "numpy>=1.21,<2", "pandas>=1.3,<2.1", "scikit_learn>=1.0", "scipy>=1.7", From 3741784d330f3d7fb78cdbeba39c9072666a6da3 Mon Sep 17 00:00:00 2001 From: Enning Yang Date: Mon, 4 May 2026 10:54:54 -0400 Subject: [PATCH 5/5] CI: drop Python 3.12 from matrix while pandas is pinned <2.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pandas 2.0.x has no Python 3.12 wheels (3.12 support landed in pandas 2.1), so pip falls back to building from source — which fails because the build env doesn't have pkg_resources. With pandas pinned <2.1 (needed for the abagen groupby workaround), 3.12 simply can't be satisfied. Drop it from the matrix; add back once abagen is patched. --- .github/workflows/python_unittests.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python_unittests.yml b/.github/workflows/python_unittests.yml index 3fe8d8e4..3e3b358c 100644 --- a/.github/workflows/python_unittests.yml +++ b/.github/workflows/python_unittests.yml @@ -11,7 +11,11 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.10', '3.11', '3.12'] + # Python 3.12 cannot be exercised while pandas is pinned <2.1 (the + # abagen groupby/pkg_resources workaround), because pandas 2.0.x + # ships no wheels for 3.12 and cannot be built from source there. + # Re-add 3.12 once abagen is patched and the pin is lifted. + python-version: ['3.10', '3.11'] os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }}