Skip to content

Commit b35a9d5

Browse files
committed
Backport PR #3607: (chore): derive CI matrix from hatch env
1 parent e1fd263 commit b35a9d5

5 files changed

Lines changed: 102 additions & 95 deletions

File tree

.github/workflows/ci.yml

Lines changed: 59 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -17,43 +17,49 @@ defaults:
1717
shell: bash -e {0} # -e to fail on error
1818

1919
jobs:
20-
pytest:
20+
get-environments:
21+
runs-on: ubuntu-latest
22+
outputs:
23+
envs: ${{ steps.get-envs.outputs.envs }}
24+
steps:
25+
- uses: actions/checkout@v4
26+
with:
27+
filter: blob:none
28+
fetch-depth: 0
29+
- uses: astral-sh/setup-uv@v5
30+
with:
31+
enable-cache: false
32+
- id: get-envs
33+
run: |
34+
ENVS_JSON=$(NO_COLOR=1 uvx hatch env show --json | jq -c 'to_entries
35+
| map(
36+
select(.key | startswith("hatch-test"))
37+
| {
38+
name: .key,
39+
"test-type": (if (.key | test("pre|min")) then "coverage" else null end),
40+
python: .value.python,
41+
}
42+
)')
43+
echo "envs=${ENVS_JSON}" | tee $GITHUB_OUTPUT
44+
45+
test:
46+
needs: get-environments
2147
runs-on: ubuntu-latest
22-
2348
strategy:
2449
matrix:
25-
include:
26-
- python-version: '3.10'
27-
- python-version: '3.12'
28-
- python-version: '3.12'
29-
dependencies-version: min-optional
30-
- python-version: '3.12'
31-
dependencies-version: pre-release
32-
test-type: coverage
33-
- python-version: '3.10'
34-
dependencies-version: minimum
35-
test-type: coverage
36-
37-
env: # for use codecov’s env_vars tagging
38-
PYTHON: ${{ matrix.python-version }}
39-
DEPS: ${{ matrix.dependencies-version || 'default' }}
40-
TESTS: ${{ matrix.test-type || 'default' }}
41-
50+
env: ${{ fromJSON(needs.get-environments.outputs.envs) }}
51+
env: # environment variable for use in codecov’s env_vars tagging
52+
ENV_NAME: ${{ matrix.env.name }}
4253
steps:
4354
- uses: actions/checkout@v4
4455
with:
4556
fetch-depth: 0
4657
filter: blob:none
4758

48-
- name: Set up Python ${{ matrix.python-version }}
49-
uses: actions/setup-python@v5
50-
with:
51-
python-version: ${{ matrix.python-version }}
52-
53-
- name: Install UV
54-
uses: astral-sh/setup-uv@v5
59+
- uses: astral-sh/setup-uv@v5
5560
with:
5661
enable-cache: true
62+
python-version: ${{ matrix.env.python }}
5763
cache-dependency-glob: pyproject.toml
5864

5965
- name: Cache downloaded data
@@ -63,55 +69,42 @@ jobs:
6369
key: pytest
6470

6571
- name: Install dependencies
66-
if: matrix.dependencies-version == null
67-
run: uv pip install --system --compile "scanpy[dev,test-full] @ ."
68-
- name: Install dependencies (no optional features)
69-
if: matrix.dependencies-version == 'min-optional'
70-
run: uv pip install --system --compile "scanpy[dev,test-min] @ ."
71-
- name: Install dependencies (minimum versions)
72-
if: matrix.dependencies-version == 'minimum'
73-
run: |
74-
uv pip install --system --compile tomli packaging
75-
deps=$(python3 ci/scripts/min-deps.py pyproject.toml --extra dev test)
76-
uv pip install --system --compile $deps "scanpy @ ."
77-
- name: Install dependencies (pre-release versions)
78-
if: matrix.dependencies-version == 'pre-release'
79-
run: uv pip install -v --system --compile --pre "scanpy[dev,test-full] @ ." "anndata[dev,test] @ git+https://github.com/scverse/anndata.git"
72+
run: uvx hatch -v env create ${{ matrix.env.name }}
8073

81-
- name: Run pytest
82-
if: matrix.test-type == null
83-
run: pytest
84-
- name: Run pytest (coverage)
85-
if: matrix.test-type == 'coverage'
86-
run: coverage run -m pytest --cov --cov-report=xml
74+
- name: Run tests
75+
if: matrix.env.test-type == null
76+
run: uvx hatch run ${{ matrix.env.name }}:run
77+
- name: Run tests (coverage)
78+
if: matrix.env.test-type == 'coverage'
79+
run: uvx hatch run ${{ matrix.env.name }}:run-cov --cov --cov-report=xml
8780

8881
- name: Upload coverage data
89-
uses: codecov/codecov-action@v4
90-
if: matrix.test-type == 'coverage'
82+
uses: codecov/codecov-action@v5
83+
if: matrix.env.test-type == 'coverage'
9184
with:
9285
token: ${{ secrets.CODECOV_TOKEN }}
93-
env_vars: "PYTHON,DEPS,TESTS"
86+
env_vars: ENV_NAME
9487
fail_ci_if_error: true
95-
file: test-data/coverage.xml
88+
files: test-data/coverage.xml
9689

9790
- name: Upload test results
9891
# yaml strings can’t start with “!”, so using explicit substitution
9992
if: ${{ !cancelled() }}
10093
uses: codecov/test-results-action@v1
10194
with:
10295
token: ${{ secrets.CODECOV_TOKEN }}
103-
env_vars: "PYTHON,DEPS,TESTS"
96+
env_vars: ENV_NAME
10497
fail_ci_if_error: true
10598
file: test-data/test-results.xml
10699

107100
- name: Publish debug artifacts
108-
if: matrix.test-type == 'coverage'
101+
if: matrix.env.test-type == 'coverage'
109102
uses: actions/upload-artifact@v4
110103
with:
111-
name: debug-data-${{ matrix.python-version }}-${{ matrix.dependencies-version || 'default' }}-${{ matrix.test-type || 'default' }}
104+
name: debug-data-${{ matrix.env.name }}
112105
path: .pytest_cache/d/debug
113106

114-
check-build:
107+
build:
115108
runs-on: ubuntu-latest
116109
steps:
117110
- uses: actions/checkout@v4
@@ -126,3 +119,15 @@ jobs:
126119
enable-cache: false
127120
- run: uvx --from build pyproject-build --sdist --wheel .
128121
- run: uvx twine check dist/*
122+
123+
check:
124+
if: always()
125+
needs:
126+
- get-environments
127+
- test
128+
- build
129+
runs-on: ubuntu-latest
130+
steps:
131+
- uses: re-actors/alls-green@release/v1
132+
with:
133+
jobs: ${{ toJSON(needs) }}

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
# Python build files
2929
__pycache__/
30-
/ci/scanpy-min-deps.txt
30+
/ci/scanpy-low-vers.txt
3131
/dist/
3232
/*-env/
3333
/env-*/
Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# "packaging",
66
# ]
77
# ///
8-
"""Parse a pyproject.toml file and output a list of minimum dependencies."""
8+
"""Parse a pyproject.toml file and output a list of minimum dependency versions."""
99

1010
from __future__ import annotations
1111

@@ -37,8 +37,9 @@ def min_dep(req: Requirement) -> Requirement:
3737
Example
3838
-------
3939
>>> min_dep(Requirement("numpy>=1.0"))
40-
<Requirement('numpy==1.0.*')>
41-
40+
<Requirement('numpy==1.0')>
41+
>>> min_dep(Requirement("numpy<3.0"))
42+
<Requirement('numpy<3.0')>
4243
"""
4344
req_name = req.name
4445
if req.extras:
@@ -48,25 +49,26 @@ def min_dep(req: Requirement) -> Requirement:
4849
spec for spec in req.specifier if spec.operator in {"==", "~=", ">=", ">"}
4950
]
5051
if not filter_specs:
51-
return Requirement(req_name)
52-
52+
# TODO: handle markers
53+
return Requirement(f"{req_name}{req.specifier}")
5354
min_version = Version("0.0.0.a1")
5455
for spec in filter_specs:
5556
if spec.operator in {">", ">=", "~="}:
5657
min_version = max(min_version, Version(spec.version))
5758
elif spec.operator == "==":
5859
min_version = Version(spec.version)
5960

60-
return Requirement(f"{req_name}=={min_version}.*")
61+
return Requirement(f"{req_name}=={min_version}")
6162

6263

6364
def extract_min_deps(
6465
dependencies: Iterable[Requirement], *, pyproject
6566
) -> Generator[Requirement, None, None]:
66-
"""Extract minimum dependencies from a list of requirements."""
67+
"""Extract minimum dependency versions from a list of requirements."""
6768
dependencies = deque(dependencies) # We'll be mutating this
6869
project_name = pyproject["project"]["name"]
6970

71+
deps = {}
7072
while len(dependencies) > 0:
7173
req = dependencies.pop()
7274

@@ -79,7 +81,11 @@ def extract_min_deps(
7981
extra_deps = pyproject["project"]["optional-dependencies"][extra]
8082
dependencies += map(Requirement, extra_deps)
8183
else:
82-
yield min_dep(req)
84+
if req.name in deps:
85+
req.specifier &= deps[req.name].specifier
86+
req.extras |= deps[req.name].extras
87+
deps[req.name] = min_dep(req)
88+
yield from deps.values()
8389

8490

8591
class Args(argparse.Namespace):
@@ -105,6 +111,7 @@ def parser(cls) -> argparse.ArgumentParser:
105111
prog="min-deps",
106112
description=cls.__doc__,
107113
usage="pip install `python min-deps.py pyproject.toml`",
114+
allow_abbrev=False,
108115
)
109116
parser.add_argument(
110117
"_path",

hatch.toml

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,27 @@ scripts.clean = "git restore --source=HEAD --staged --worktree -- docs/release-n
1515

1616
[envs.hatch-test]
1717
default-args = [ ]
18-
features = [ "dev", "test", "dask-ml" ]
18+
features = [ "dev", "test-min" ]
1919
extra-dependencies = [ "ipykernel" ]
2020
overrides.matrix.deps.env-vars = [
2121
{ if = [ "pre" ], key = "UV_PRERELEASE", value = "allow" },
22-
{ if = [ "min" ], key = "UV_CONSTRAINT", value = "ci/scanpy-min-deps.txt" },
22+
{ if = [ "low-vers" ], key = "UV_CONSTRAINT", value = "ci/scanpy-low-vers.txt" },
2323
]
2424
overrides.matrix.deps.pre-install-commands = [
25-
{ if = [ "min" ], value = "uv run ci/scripts/min-deps.py pyproject.toml --all-extras -o ci/scanpy-min-deps.txt" },
25+
{ if = [
26+
"low-vers",
27+
], value = "uv run ci/scripts/low-vers.py pyproject.toml --all-extras -o ci/scanpy-low-vers.txt" },
2628
]
2729
overrides.matrix.deps.python = [
28-
{ if = [ "min" ], value = "3.10" },
29-
{ if = [ "stable", "full", "pre" ], value = "3.12" },
30-
]
31-
overrides.matrix.deps.features = [
32-
{ if = [ "full" ], value = "test-full" },
30+
{ if = [ "low-vers" ], value = "3.10" },
31+
{ if = [ "stable", "pre" ], value = "3.12" },
3332
]
3433
overrides.matrix.deps.extra-dependencies = [
3534
{ if = [ "pre" ], value = "anndata[dev,test] @ git+https://github.com/scverse/anndata.git" },
3635
]
36+
overrides.matrix.deps.features = [
37+
{ if = [ "stable", "pre", "low-vers" ], value = "test" },
38+
]
3739

3840
[[envs.hatch-test.matrix]]
39-
deps = [ "stable", "full", "pre", "min" ]
41+
deps = [ "stable", "pre", "low-vers", "few-extras" ]

pyproject.toml

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,18 @@ dependencies = [
5454
"seaborn>=0.13",
5555
"h5py>=3.7",
5656
"tqdm",
57-
"scikit-learn>=1.1",
58-
"statsmodels>=0.14",
57+
"scikit-learn>=1.1.3",
58+
"statsmodels>=0.14.4",
5959
"patsy!=1.0.0", # https://github.com/pydata/patsy/issues/215
6060
"networkx>=2.7",
6161
"natsort",
6262
"joblib",
6363
"numba>=0.57",
64-
"umap-learn>=0.5,!=0.5.0",
64+
"umap-learn>=0.5.13",
6565
"pynndescent>=0.5",
6666
"packaging>=21.3",
6767
"session-info2",
68-
"legacy-api-wrap>=1.4", # for positional API deprecations
68+
"legacy-api-wrap>=1.4.1", # for positional API deprecations
6969
"typing-extensions; python_version < '3.13'",
7070
]
7171
dynamic = [ "version" ]
@@ -94,22 +94,15 @@ test-min = [
9494
]
9595
test = [
9696
"scanpy[test-min]",
97-
# Optional but important dependencies
98-
"scanpy[leiden]",
99-
"zarr<3",
97+
# optional storage and processing modes
10098
"scanpy[dask]",
101-
"scanpy[scrublet]",
102-
]
103-
test-full = [
104-
"scanpy[test]",
105-
# optional storage modes
10699
"zappy",
100+
"zarr<3",
107101
# additional tested algorithms
102+
"scanpy[scrublet]",
108103
"scanpy[louvain]",
109-
"scanpy[magic]",
104+
"scanpy[leiden]",
110105
"scanpy[skmisc]",
111-
"scanpy[harmony]",
112-
"scanpy[scanorama]",
113106
"scanpy[dask-ml]",
114107
]
115108
doc = [
@@ -139,14 +132,14 @@ dev = [
139132
]
140133
# Algorithms
141134
paga = [ "igraph" ]
142-
louvain = [ "igraph", "louvain>=0.6.0,!=0.6.2" ] # Louvain community detection
143-
leiden = [ "igraph>=0.10", "leidenalg>=0.9.0" ] # Leiden community detection
144-
bbknn = [ "bbknn" ] # Batch balanced KNN (batch correction)
145-
magic = [ "magic-impute>=2.0" ] # MAGIC imputation method
146-
skmisc = [ "scikit-misc>=0.1.3" ] # highly_variable_genes method 'seurat_v3'
147-
harmony = [ "harmonypy" ] # Harmony dataset integration
148-
scanorama = [ "scanorama" ] # Scanorama dataset integration
149-
scrublet = [ "scikit-image" ] # Doublet detection with automatic thresholds
135+
louvain = [ "igraph", "louvain>=0.6.0,!=0.6.2" ] # Louvain community detection
136+
leiden = [ "igraph>=0.10.8", "leidenalg>=0.9.0" ] # Leiden community detection
137+
bbknn = [ "bbknn" ] # Batch balanced KNN (batch correction)
138+
magic = [ "magic-impute>=2.0.4" ] # MAGIC imputation method
139+
skmisc = [ "scikit-misc>=0.1.3" ] # highly_variable_genes method 'seurat_v3'
140+
harmony = [ "harmonypy" ] # Harmony dataset integration
141+
scanorama = [ "scanorama" ] # Scanorama dataset integration
142+
scrublet = [ "scikit-image" ] # Doublet detection with automatic thresholds
150143
# Acceleration
151144
rapids = [ "cudf>=0.9", "cuml>=0.9", "cugraph>=0.9" ] # GPU accelerated calculation of neighbors
152145
dask = [ "dask[array]>=2022.09.2" ] # Use the Dask parallelization engine

0 commit comments

Comments
 (0)