diff --git a/.gitignore b/.gitignore index beafaf617..d21120ee9 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ # Python build files __pycache__/ /src/scanpy/_version.py +/ci/scanpy-min-deps.txt /dist/ /*-env/ /env-*/ diff --git a/ci/scripts/min-deps.py b/ci/scripts/min-deps.py index 4dad297e0..b996302c0 100755 --- a/ci/scripts/min-deps.py +++ b/ci/scripts/min-deps.py @@ -11,6 +11,7 @@ import argparse import sys from collections import deque +from contextlib import ExitStack from pathlib import Path from typing import TYPE_CHECKING @@ -23,7 +24,7 @@ from packaging.version import Version if TYPE_CHECKING: - from collections.abc import Generator, Iterable + from collections.abc import Generator, Iterable, Sequence def min_dep(req: Requirement) -> Requirement: @@ -75,12 +76,19 @@ def extract_min_deps( yield min_dep(req) -def main(): +class Args(argparse.Namespace): + path: Path + output: Path | None + extras: list[str] + + +def main(argv: Sequence[str] | None = None) -> None: parser = argparse.ArgumentParser( prog="min-deps", - description="""Parse a pyproject.toml file and output a list of minimum dependencies. - - Output is directly passable to `pip install`.""", + description=( + "Parse a pyproject.toml file and output a list of minimum dependencies. " + "Output is optimized for `[uv] pip install` (see `-o`/`--output` for details)." + ), usage="pip install `python min-deps.py pyproject.toml`", ) parser.add_argument( @@ -89,8 +97,18 @@ def main(): parser.add_argument( "--extras", type=str, nargs="*", default=(), help="extras to install" ) + parser.add_argument( + *("--output", "-o"), + type=Path, + default=None, + help=( + "output file (default: stdout). " + "Without this option, output is space-separated for direct passing to `pip install`. " + "With this option, output written to a file newline-separated file usable as `requirements.txt` or `constraints.txt`." + ), + ) - args = parser.parse_args() + args = parser.parse_args(argv, Args()) pyproject = tomllib.loads(args.path.read_text()) @@ -102,7 +120,10 @@ def main(): min_deps = extract_min_deps(deps, pyproject=pyproject) - print(" ".join(map(str, min_deps))) + sep = "\n" if args.output else " " + with ExitStack() as stack: + f = stack.enter_context(args.output.open("w")) if args.output else sys.stdout + print(sep.join(map(str, min_deps)), file=f) if __name__ == "__main__": diff --git a/ci/scripts/towncrier_automation.py b/ci/scripts/towncrier_automation.py index 57a093a30..fd492f494 100755 --- a/ci/scripts/towncrier_automation.py +++ b/ci/scripts/towncrier_automation.py @@ -1,4 +1,8 @@ #!/usr/bin/env python3 +# /// script +# dependencies = [ "towncrier", "packaging" ] +# /// + from __future__ import annotations import argparse diff --git a/hatch.toml b/hatch.toml index ab2bb7550..ad5db6097 100644 --- a/hatch.toml +++ b/hatch.toml @@ -19,14 +19,17 @@ features = ["test", "dask-ml"] extra-dependencies = ["ipykernel"] overrides.matrix.deps.env-vars = [ { if = ["pre"], key = "UV_PRERELEASE", value = "allow" }, - { if = ["min"], key = "UV_RESOLUTION", value = "lowest-direct" }, + { if = ["min"], key = "UV_CONSTRAINT", value = "ci/scanpy-min-deps.txt" }, +] +overrides.matrix.deps.pre-install-commands = [ + { if = ["min"], value = "uv run ci/scripts/min-deps.py pyproject.toml -o ci/scanpy-min-deps.txt" }, ] overrides.matrix.deps.python = [ - { if = ["min"] , value = "3.10" }, + { if = ["min"], value = "3.10" }, { if = ["stable", "full", "pre"], value = "3.12" }, ] overrides.matrix.deps.features = [ - { if = ["full"] , value = "test-full" }, + { if = ["full"], value = "test-full" }, ] [[envs.hatch-test.matrix]] diff --git a/pyproject.toml b/pyproject.toml index 8d221396b..d7e790f09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ dependencies = [ "pandas >=1.5", "scipy>=1.8", "seaborn>=0.13", - "h5py>=3.6", + "h5py>=3.7", "tqdm", "scikit-learn>=1.1", "statsmodels>=0.13",