Skip to content

Commit 1c4e7cf

Browse files
committed
Harden release boundaries and fix test contracts
1 parent 31540ec commit 1c4e7cf

8 files changed

Lines changed: 100 additions & 30 deletions

File tree

.github/workflows/python-app.yml

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,7 @@ jobs:
501501
# 所以必须在 shell 命令内临时设置,delocate 才能找到 libomp.dylib。
502502
# 2. 添加 --require-archs {delocate_archs} 确保打包的 dylib 架构
503503
# 与 .so 文件一致(防止 arm64/x86_64 混淆导致用户端 crash)。
504+
# 3. wheel repair 失败时必须让 CI 失败,不能发布未打包 libomp 的 macOS wheel。
504505
CIBW_REPAIR_WHEEL_COMMAND_MACOS: |
505506
echo "=== Repairing macOS wheel ==="
506507
echo "Current architecture: $(uname -m)"
@@ -510,13 +511,8 @@ jobs:
510511
# 在 repair 命令内部设置 DYLD_LIBRARY_PATH,绕过 macOS SIP 的剥离
511512
DYLD_LIBRARY_PATH="${REPAIR_LIBRARY_PATH}:${DYLD_LIBRARY_PATH:-}" \
512513
delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} || {
513-
echo "[WARNING] delocate with --require-archs failed, retrying without arch check..."
514-
DYLD_LIBRARY_PATH="${REPAIR_LIBRARY_PATH}:${DYLD_LIBRARY_PATH:-}" \
515-
delocate-wheel -w {dest_dir} -v {wheel} || {
516-
echo "[WARNING] delocate failed completely, copying wheel as-is"
517-
mkdir -p {dest_dir}
518-
cp {wheel} {dest_dir}/
519-
}
514+
echo "[ERROR] delocate failed; refusing to publish an unrepaired macOS wheel."
515+
exit 1
520516
}
521517
522518
# 验证修复结果:确认 libomp 是否真的打包进去了
@@ -528,7 +524,8 @@ jobs:
528524
if delocate-listdeps --all "$REPAIRED_WHEEL" 2>&1 | grep -q "libomp"; then
529525
echo "[OK] libomp successfully bundled into wheel"
530526
else
531-
echo "[WARNING] libomp may not be bundled - check wheel manually"
527+
echo "[ERROR] libomp is not bundled into the macOS wheel."
528+
exit 1
532529
fi
533530
fi
534531
@@ -544,9 +541,15 @@ jobs:
544541
python -m pip install --upgrade pip
545542
echo "Ensuring clean build environment..."
546543
547-
# 再次清理,确保没有残留的构建文件
548-
find . -name "*.so" -delete 2>/dev/null || true
549-
find . -name "*.c" -delete 2>/dev/null || true
544+
# 再次清理,确保没有残留的项目构建文件;不要全仓库扫描依赖/实验目录。
545+
find ./sequenzo -name "*.so" -type f -delete 2>/dev/null || true
546+
find . -maxdepth 1 -name "*.so" -type f -delete 2>/dev/null || true
547+
find ./sequenzo -path "*/utils/*.c" -type f -print0 | while IFS= read -r -d '' cfile; do
548+
pyxfile="${cfile%.c}.pyx"
549+
if [ -f "$pyxfile" ]; then
550+
rm -f "$cfile"
551+
fi
552+
done
550553
rm -rf build/ dist/ *.egg-info
551554
552555
echo "Clean build environment ready"
@@ -567,11 +570,10 @@ jobs:
567570
echo "=== Attempting wheel repair ==="
568571
echo "Wheel to repair: {wheel}"
569572
570-
# 尝试修复,如果失败则跳过修复
573+
# wheel repair 失败时必须让 CI 失败,不能发布未修复 wheel。
571574
auditwheel repair -w {dest_dir} {wheel} || {
572-
echo "[WARNING] auditwheel repair failed, copying wheel as-is"
573-
mkdir -p {dest_dir}
574-
cp {wheel} {dest_dir}/
575+
echo "[ERROR] auditwheel repair failed; refusing to publish an unrepaired Linux wheel."
576+
exit 1
575577
}
576578
577579
# 检查修复结果
@@ -1289,4 +1291,4 @@ jobs:
12891291
uses: actions/upload-artifact@v4
12901292
with:
12911293
name: wheels-${{ matrix.os }}-${{ matrix.python-version }}
1292-
path: dist/
1294+
path: dist/

MANIFEST.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ recursive-include sequenzo/dissimilarity_measures/src *.hpp
1313
recursive-include sequenzo/clustering/src *.cpp
1414
recursive-include sequenzo/clustering/src *.h
1515
recursive-include sequenzo/clustering/src *.hpp
16+
recursive-include sequenzo/clustering/fuzzy_clustering/src *.cpp
17+
recursive-include sequenzo/clustering/fuzzy_clustering/src *.h
18+
recursive-include sequenzo/clustering/fuzzy_clustering/src *.hpp
1619

1720
# Include all header files and external libraries
1821
recursive-include sequenzo/dissimilarity_measures/src/xsimd/include *.hpp

pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ include-package-data = true
8080
]
8181
"sequenzo.clustering" = [
8282
"src/**/*.cpp",
83-
"src/**/*.h"
83+
"src/**/*.h",
84+
"fuzzy_clustering/src/**/*.cpp",
85+
"fuzzy_clustering/src/**/*.h"
8486
]
8587
"sequenzo.clustering.sequenzo_fastcluster" = [
8688
"src/**/*.cpp",
@@ -102,6 +104,9 @@ include = ["sequenzo*"]
102104
include = ["pybind11"]
103105

104106
[tool.pytest.ini_options]
107+
testpaths = [
108+
"tests",
109+
]
105110
norecursedirs = [
106111
".git",
107112
".venv",

sequenzo/decomposition/sa_kob.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
@Author : Yuqi Liang 梁彧祺
2+
@Author : Yuqi Liang 梁彧祺; Yapeng Wei 卫亚鹏
33
@File : sa_kob.py
44
@Time : 2026-04-20 16:30
55
@Desc :
@@ -343,10 +343,12 @@ def detect_cluster_coefficient_owners(
343343
override_key = category_id
344344

345345
if override_key is not None:
346-
owner = _validate_coefficient_owner(
347-
int(owner_overrides[override_key]),
348-
context="[detect_cluster_coefficient_owners] owner_overrides",
349-
)
346+
owner = int(owner_overrides[override_key])
347+
if owner not in (-1, 0, 1):
348+
raise ValueError(
349+
"[detect_cluster_coefficient_owners] owner_overrides must map "
350+
"cluster labels or category ids to -1, 0, or 1."
351+
)
350352
classification = "override"
351353
elif gap_percent > majority_gap_threshold:
352354
owner = 0 if share0 > share1 else 1

sequenzo/seqhmm/formulas.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,12 +264,13 @@ def create_model_matrix_time_constant(
264264
f"Variable '{term}' not found in data columns: {list(data.columns)}"
265265
)
266266

267-
covar_values = data[term].values
267+
series = data[term]
268+
covar_values = series.values
268269

269270
# Check if this is a categorical variable
270-
if pd.api.types.is_categorical_dtype(data[term]) or \
271-
pd.api.types.is_object_dtype(data[term]) or \
272-
(data[term].dtype == 'object'):
271+
if isinstance(series.dtype, pd.CategoricalDtype) or \
272+
pd.api.types.is_object_dtype(series) or \
273+
pd.api.types.is_string_dtype(series):
273274
# Categorical variable: create dummy variables
274275
# Use pandas get_dummies to create dummies, drop first level as reference
275276
dummies = pd.get_dummies(data[[term]], prefix=term, drop_first=True)

tests/compare_differences/test_sa_kob_decomposition.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ def test_invalid_cluster_coefficient_reference_raises():
309309
cluster_labels=cluster_labels,
310310
k=4,
311311
categories=[1, 2, 3, 4],
312-
cluster_coefficient_reference="pooled",
312+
cluster_coefficient_reference="invalid",
313313
)
314314

315315

tests/seqHMM/advanced/test_seqhmm_advanced.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,9 @@ def test_create_matrix_time_constant_categorical(self):
331331
"color": ["red", "blue", "green", "red", "blue", "green"],
332332
})
333333
X = create_model_matrix_time_constant("~ color", data, n_seq)
334-
# color has 3 levels, drop_first=True -> 2 dummy columns
335-
# (no separate intercept column added by this function)
336-
assert X.shape == (n_seq, 2) # 2 dummies
334+
# intercept + color's 3 levels with drop_first=True -> 3 columns
335+
assert X.shape == (n_seq, 3)
336+
assert np.allclose(X[:, 0], 1.0)
337337

338338

339339
# ============================================================================
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from pathlib import Path
2+
3+
try:
4+
import tomllib
5+
except ModuleNotFoundError: # Python 3.9/3.10
6+
import tomli as tomllib
7+
8+
9+
def test_setup_does_not_clone_unpinned_xsimd_from_network():
10+
setup_py = Path(__file__).resolve().parents[1] / "setup.py"
11+
source = setup_py.read_text()
12+
13+
assert "git\", \"clone" not in source
14+
assert "https://github.com/xtensor-stack/xsimd.git" not in source
15+
assert "include/xsimd/xsimd.hpp" in source
16+
17+
18+
def test_macos_wheel_repair_fails_instead_of_copying_unrepaired_wheel():
19+
workflow = Path(__file__).resolve().parents[1] / ".github" / "workflows" / "python-app.yml"
20+
source = workflow.read_text()
21+
22+
assert "copying wheel as-is" not in source
23+
assert "cp {wheel} {dest_dir}/" not in source
24+
assert 'find . -name "*.c" -delete' not in source
25+
assert 'find . -name "*.so" -delete' not in source
26+
assert "delocate-wheel --require-archs {delocate_archs}" in source
27+
assert "exit 1" in source
28+
29+
30+
def test_setup_builds_fanny_cpp_binding_without_fast_math():
31+
setup_py = Path(__file__).resolve().parents[1] / "setup.py"
32+
source = setup_py.read_text()
33+
34+
assert "'sequenzo/clustering/fuzzy_clustering/src/'" in source
35+
assert "'sequenzo/clustering/fuzzy_clustering/src/fanny.cpp'" in source
36+
assert "or 'fanny.cpp' in src" in source
37+
38+
39+
def test_sdist_includes_fanny_cpp_sources_required_by_setup():
40+
repo_root = Path(__file__).resolve().parents[1]
41+
manifest = (repo_root / "MANIFEST.in").read_text()
42+
config = tomllib.loads((repo_root / "pyproject.toml").read_text())
43+
44+
assert "recursive-include sequenzo/clustering/fuzzy_clustering/src *.cpp" in manifest
45+
assert "recursive-include sequenzo/clustering/fuzzy_clustering/src *.h" in manifest
46+
47+
package_data = config["tool"]["setuptools"]["package-data"]["sequenzo.clustering"]
48+
assert "fuzzy_clustering/src/**/*.cpp" in package_data
49+
assert "fuzzy_clustering/src/**/*.h" in package_data
50+
51+
52+
def test_pytest_collects_only_project_tests_by_default():
53+
pyproject = Path(__file__).resolve().parents[1] / "pyproject.toml"
54+
config = tomllib.loads(pyproject.read_text())
55+
56+
pytest_config = config["tool"]["pytest"]["ini_options"]
57+
assert pytest_config["testpaths"] == ["tests"]

0 commit comments

Comments
 (0)