Skip to content

Commit a1be4da

Browse files
committed
Fix python version support
1 parent f12fd5f commit a1be4da

5 files changed

Lines changed: 153 additions & 20 deletions

File tree

.github/workflows/python-publish.yml

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ jobs:
4444
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
4545

4646
steps:
47-
- uses: actions/checkout@v6
47+
- uses: actions/checkout@v4
4848

49-
- uses: actions/setup-python@v6
49+
- uses: actions/setup-python@v5
5050
with:
5151
python-version: ${{ matrix.python-version }}
5252

@@ -58,6 +58,11 @@ jobs:
5858
- name: Build and install (including C extension)
5959
run: pip install -e . --no-build-isolation
6060

61+
- name: Install optional dependencies (best-effort)
62+
run: |
63+
pip install xgboost || echo "xgboost unavailable for this Python — xgb tests will be skipped"
64+
pip install "torch>=2.0" --index-url https://download.pytorch.org/whl/cpu || echo "torch unavailable for this Python — DL tests will be skipped"
65+
6166
- name: Run test suite (excluding slow/perf)
6267
run: |
6368
pytest \
@@ -67,14 +72,14 @@ jobs:
6772
6873
- name: Upload test results
6974
if: always()
70-
uses: actions/upload-artifact@v6
75+
uses: actions/upload-artifact@v4
7176
with:
7277
name: test-results-py${{ matrix.python-version }}
7378
path: test-results.xml
7479

7580
- name: Upload coverage report
7681
if: always()
77-
uses: actions/upload-artifact@v6
82+
uses: actions/upload-artifact@v4
7883
with:
7984
name: coverage-py${{ matrix.python-version }}
8085
path: coverage.xml
@@ -88,9 +93,9 @@ jobs:
8893
runs-on: ubuntu-latest
8994

9095
steps:
91-
- uses: actions/checkout@v6
96+
- uses: actions/checkout@v4
9297

93-
- uses: actions/setup-python@v6
98+
- uses: actions/setup-python@v5
9499
with:
95100
python-version: "3.12"
96101

@@ -99,7 +104,7 @@ jobs:
99104
pip install build
100105
python -m build --sdist
101106
102-
- uses: actions/upload-artifact@v6
107+
- uses: actions/upload-artifact@v4
103108
with:
104109
name: sdist
105110
path: dist/*.tar.gz
@@ -120,15 +125,15 @@ jobs:
120125
os: [ubuntu-latest, macos-latest, windows-latest]
121126

122127
steps:
123-
- uses: actions/checkout@v6
128+
- uses: actions/checkout@v4
124129

125130
- name: Set up QEMU (Linux aarch64 cross-compilation)
126131
if: matrix.os == 'ubuntu-latest'
127132
uses: docker/setup-qemu-action@v3
128133
with:
129134
platforms: arm64
130135

131-
- uses: actions/setup-python@v6
136+
- uses: actions/setup-python@v5
132137
with:
133138
python-version: "3.12"
134139

@@ -151,7 +156,7 @@ jobs:
151156
CIBW_BUILD_VERBOSITY: 1
152157
run: cibuildwheel --output-dir wheelhouse
153158

154-
- uses: actions/upload-artifact@v6
159+
- uses: actions/upload-artifact@v4
155160
with:
156161
name: wheels-${{ matrix.os }}
157162
path: wheelhouse/*.whl
@@ -165,16 +170,16 @@ jobs:
165170
runs-on: ubuntu-latest
166171

167172
steps:
168-
- uses: actions/checkout@v6
173+
- uses: actions/checkout@v4
169174

170175
- name: Download source distribution
171-
uses: actions/download-artifact@v6
176+
uses: actions/download-artifact@v4
172177
with:
173178
name: sdist
174179
path: dist/
175180

176181
- name: Download wheels
177-
uses: actions/download-artifact@v6
182+
uses: actions/download-artifact@v4
178183
with:
179184
pattern: "wheels-*"
180185
path: dist/
@@ -209,13 +214,13 @@ jobs:
209214

210215
steps:
211216
- name: Download source distribution
212-
uses: actions/download-artifact@v6
217+
uses: actions/download-artifact@v4
213218
with:
214219
name: sdist
215220
path: dist/
216221

217222
- name: Download wheels
218-
uses: actions/download-artifact@v6
223+
uses: actions/download-artifact@v4
219224
with:
220225
pattern: "wheels-*"
221226
path: dist/
@@ -236,7 +241,7 @@ jobs:
236241
runs-on: ubuntu-latest
237242

238243
steps:
239-
- uses: actions/checkout@v6
244+
- uses: actions/checkout@v4
240245
with:
241246
fetch-depth: 0
242247

@@ -255,13 +260,13 @@ jobs:
255260
RELEASE_TAG: ${{ env.RELEASE_TAG }}
256261

257262
- name: Download source distribution
258-
uses: actions/download-artifact@v6
263+
uses: actions/download-artifact@v4
259264
with:
260265
name: sdist
261266
path: release-assets/
262267

263268
- name: Download wheels
264-
uses: actions/download-artifact@v6
269+
uses: actions/download-artifact@v4
265270
with:
266271
pattern: "wheels-*"
267272
path: release-assets/

Dockerfile.test

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Dockerfile.test — mirrors the GitHub Actions CI test job exactly.
2+
#
3+
# Build:
4+
# docker build -f Dockerfile.test -t metbit-test . # default Python 3.12
5+
# docker build -f Dockerfile.test --build-arg PYTHON_VERSION=3.11 -t metbit-test-311 .
6+
#
7+
# Run:
8+
# docker run --rm metbit-test # full suite
9+
# docker run --rm metbit-test pytest -k pca # filter
10+
# docker run --rm -it metbit-test bash # shell
11+
12+
ARG PYTHON_VERSION=3.12
13+
FROM python:${PYTHON_VERSION}-slim
14+
15+
WORKDIR /src
16+
17+
# System build deps for the C extension
18+
RUN apt-get update && apt-get install -y --no-install-recommends \
19+
gcc g++ make \
20+
&& rm -rf /var/lib/apt/lists/*
21+
22+
# -- Layer 1: dev tools (cached when requirements-dev.txt unchanged)
23+
COPY requirements-dev.txt .
24+
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
25+
pip install --no-cache-dir -r requirements-dev.txt
26+
27+
# -- Layer 2: optional deps (best-effort — same as CI)
28+
RUN pip install --no-cache-dir xgboost \
29+
|| echo "WARNING: xgboost unavailable on this Python — xgb tests will be skipped"
30+
RUN pip install --no-cache-dir "torch>=2.0" \
31+
--index-url https://download.pytorch.org/whl/cpu \
32+
|| echo "WARNING: torch unavailable on this Python — DL tests will be skipped"
33+
34+
# -- Layer 3: package source + C extension
35+
COPY . .
36+
RUN pip install --no-cache-dir -e . --no-build-isolation
37+
38+
# Default command matches the CI release gate exactly
39+
CMD ["pytest", "-m", "not slow and not perf", "--tb=short", "-q"]

docker-test.sh

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env bash
2+
# docker-test.sh — build and run the metbit test container for one or all Python versions.
3+
#
4+
# Usage:
5+
# ./docker-test.sh # test all versions (3.10 → 3.13)
6+
# ./docker-test.sh 3.11 # single version
7+
# ./docker-test.sh 3.12 -k pca # filter tests
8+
# ./docker-test.sh all # explicitly test all versions
9+
# KEEP=1 ./docker-test.sh 3.12 # keep container after run
10+
11+
set -euo pipefail
12+
13+
ALL_VERSIONS=("3.10" "3.11" "3.12" "3.13")
14+
15+
run_version() {
16+
local PY="$1"
17+
shift
18+
local IMAGE="metbit-test-py${PY//.}"
19+
20+
echo ""
21+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
22+
echo " Python ${PY}${IMAGE}"
23+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
24+
25+
echo "--> Building..."
26+
docker build \
27+
--build-arg PYTHON_VERSION="${PY}" \
28+
-f Dockerfile.test \
29+
-t "${IMAGE}" \
30+
. \
31+
--quiet
32+
33+
local DOCKER_OPTS="--rm"
34+
if [[ "${KEEP:-0}" == "1" ]]; then
35+
DOCKER_OPTS=""
36+
echo " (container kept — find it with 'docker ps -a')"
37+
fi
38+
39+
echo "--> Running tests..."
40+
if docker run ${DOCKER_OPTS} "${IMAGE}" \
41+
pytest -m "not slow and not perf" --tb=short -q "$@"; then
42+
echo "✓ Python ${PY} PASSED"
43+
return 0
44+
else
45+
echo "✗ Python ${PY} FAILED"
46+
return 1
47+
fi
48+
}
49+
50+
# Determine what to run
51+
ARG="${1:-all}"
52+
shift || true
53+
54+
if [[ "$ARG" == "all" ]]; then
55+
FAILED=()
56+
for PY in "${ALL_VERSIONS[@]}"; do
57+
run_version "$PY" "$@" || FAILED+=("$PY")
58+
done
59+
60+
echo ""
61+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
62+
if [[ ${#FAILED[@]} -eq 0 ]]; then
63+
echo " All versions PASSED: ${ALL_VERSIONS[*]}"
64+
else
65+
echo " FAILED versions: ${FAILED[*]}"
66+
exit 1
67+
fi
68+
else
69+
run_version "$ARG" "$@"
70+
fi

metbit/ml/classifiers.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,9 @@ def plot_roc(
451451

452452
classes = self.classes_
453453
n_classes = len(classes)
454-
y_bin = label_binarize(self._y, classes=classes)
454+
# self._y holds integer-encoded labels (0..n_classes-1); binarize against those
455+
int_classes = np.arange(n_classes)
456+
y_bin = label_binarize(self._y, classes=int_classes)
455457
if n_classes == 2:
456458
y_bin = np.hstack([1 - y_bin, y_bin])
457459

tests/test_new_modules.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,12 @@ def test_svm_fit(self, X_y_binary):
343343
clf = MLClassifier(X, y, model="svm", random_state=0).fit(cv=3)
344344
assert len(clf.predict(X)) == len(X)
345345

346-
@pytest.mark.skip(reason="XGBoost segfaults on Python 3.14 — runtime incompatibility")
346+
@pytest.mark.skipif(
347+
__import__("sys").version_info >= (3, 14),
348+
reason="xgboost 3.x segfaults under CPython 3.14 (upstream C-API incompatibility)",
349+
)
347350
def test_xgb_fit(self, X_y_binary):
351+
pytest.importorskip("xgboost", reason="xgboost not installed")
348352
from metbit.ml.classifiers import MLClassifier
349353
X, y = X_y_binary
350354
clf = MLClassifier(X, y, model="xgb", random_state=0).fit(cv=3)
@@ -359,6 +363,16 @@ def test_elasticnet_fit(self, X_y_binary):
359363

360364
# ── dl/models ────────────────────────────────────────────────────────────────
361365

366+
try:
367+
import torch as _torch_check # noqa: F401
368+
_TORCH_AVAILABLE = True
369+
except ImportError:
370+
_TORCH_AVAILABLE = False
371+
372+
_skip_no_torch = pytest.mark.skipif(not _TORCH_AVAILABLE, reason="torch not installed")
373+
374+
375+
@_skip_no_torch
362376
class TestSpectralAutoencoder:
363377
def test_fit_encode(self, X_y_binary):
364378
from metbit.dl.models import SpectralAutoencoder
@@ -399,6 +413,7 @@ def test_plot_loss(self, X_y_binary):
399413
assert isinstance(fig, go.Figure)
400414

401415

416+
@_skip_no_torch
402417
class TestSpectralMLP:
403418
def test_fit_predict(self, X_y_binary):
404419
from metbit.dl.models import SpectralMLP
@@ -448,6 +463,7 @@ def test_get_accuracy(self, X_y_binary):
448463
assert 0.0 <= acc <= 1.0
449464

450465

466+
@_skip_no_torch
451467
class TestSpectralCNN:
452468
def test_fit_predict(self, X_y_binary):
453469
from metbit.dl.models import SpectralCNN
@@ -947,6 +963,7 @@ def test_bootstrap_bad_metric_raises(self, X_y_binary):
947963
val.bootstrap_ci(metric="invalid_metric")
948964

949965

966+
@_skip_no_torch
950967
class TestDLEdgeCases:
951968
def test_autoencoder_ndarray(self):
952969
from metbit.dl.models import SpectralAutoencoder

0 commit comments

Comments
 (0)