From 444bcb3f182fe82ba6797b1e6ec174a7bc314b82 Mon Sep 17 00:00:00 2001 From: pvlov Date: Wed, 12 Aug 2026 21:14:03 +0200 Subject: [PATCH] [FIX] Replace statsmodels.robust.scale.mad with scipy.stats.median_abs_deviation to avoid enabling the GIL and remove statsmodels from dependencies --- .github/workflows/testing.yml | 2 +- meegkit/asr.py | 5 ++--- requirements.txt | 1 - tests/test_freethreading.py | 26 ++++++++++++++++++++++++++ 4 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 tests/test_freethreading.py diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index ae2f98af..57f8d2a2 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -14,7 +14,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] steps: - uses: actions/checkout@v7 diff --git a/meegkit/asr.py b/meegkit/asr.py index 2009b90a..54fe2b09 100755 --- a/meegkit/asr.py +++ b/meegkit/asr.py @@ -4,8 +4,7 @@ import numpy as np import pyriemann from pyriemann.geometry.mean import gmean as mean_covariance -from scipy import linalg, signal -from statsmodels.robust.scale import mad +from scipy import linalg, signal, stats from .utils import block_covariance, nonlinear_eigenspace from .utils.asr import ( @@ -407,7 +406,7 @@ def clean_windows(X, sfreq, max_bad_chans=0.2, zthresholds=[-3.5, 5], # extra meegkit-specific criterion: drop windows whose across-channel # z-scores are nearly flat (very low MAD or std) - bad_by_mad = mad(wz, c=1, axis=0) < .1 + bad_by_mad = stats.median_abs_deviation(wz, axis=0) < .1 bad_by_std = np.std(wz, axis=0) < .1 mask3 = np.logical_or(bad_by_mad, bad_by_std) diff --git a/requirements.txt b/requirements.txt index 878c0097..6286662b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,4 @@ scikit-learn pandas joblib tqdm -statsmodels pyriemann>=0.12 diff --git a/tests/test_freethreading.py b/tests/test_freethreading.py new file mode 100644 index 00000000..8aacbb5c --- /dev/null +++ b/tests/test_freethreading.py @@ -0,0 +1,26 @@ +import subprocess +import sys +import sysconfig + +import pytest + +pytestmark = pytest.mark.skipif( + not sysconfig.get_config_var("Py_GIL_DISABLED"), + reason="requires a free-threaded CPython build", +) + + +def test_import_does_not_reenable_gil(): + # test dependencies (such as pytest extensions) could re-enable the GIL + # without making the library non-freethreaded so we need to run this in + # its own subprocess. + proc = subprocess.run( + [ + sys.executable, + "-c", + "import sys, meegkit; raise SystemExit(int(sys._is_gil_enabled()))", + ], + capture_output=True, + text=True, + ) + assert proc.returncode == 0, f"stdout:\n{proc.stdout}\nstderr:\n{proc.stderr}"