diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index afc1b4153..ad32a3ca5 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -49,11 +49,7 @@ jobs: path: $(uv_cache_dir) displayName: Cache pip packages - # This prevents broken and slow back-tracking in dependency resolution - - script: printf "llvmlite>=0.43\nscanpy>=1.10.0rc1" | tee /tmp/constraints.txt - displayName: "Create constraints file for `pre-release` and `latest` jobs" - - - script: uv pip install --system --compile "anndata[dev,test] @ ." -c /tmp/constraints.txt + - script: uv pip install --system --compile "anndata[dev,test] @ ." -c ci/constraints.txt displayName: "Install dependencies" condition: eq(variables['DEPENDENCIES_VERSION'], 'latest') @@ -65,7 +61,7 @@ jobs: displayName: "Install minimum dependencies" condition: eq(variables['DEPENDENCIES_VERSION'], 'minimum') - - script: uv pip install -v --system --compile --pre "anndata[dev,test] @ ." -c /tmp/constraints.txt + - script: uv pip install -v --system --compile --pre "anndata[dev,test] @ ." -c ci/constraints.txt displayName: "Install dependencies release candidates" condition: eq(variables['DEPENDENCIES_VERSION'], 'pre-release') diff --git a/.github/workflows/test-gpu.yml b/.github/workflows/test-gpu.yml index 5f1d26667..1788fcf83 100644 --- a/.github/workflows/test-gpu.yml +++ b/.github/workflows/test-gpu.yml @@ -72,9 +72,7 @@ jobs: cache-dependency-path: pyproject.toml - name: Install AnnData - run: | - printf "llvmlite>=0.43\nscanpy>=1.10.0rc1" | tee /tmp/constraints.txt - uv pip install --system -e ".[dev,test,cu12]" -c /tmp/constraints.txt + run: uv pip install --system -e ".[dev,test,cu12]" -c ci/constraints.txt - name: Env list run: pip list diff --git a/.gitignore b/.gitignore index 8124b9450..7bbcff698 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ __pycache__/ # Distribution / packaging /dist/ +/ci/min-deps.txt /src/anndata/_version.py /requirements*.lock /.python-version diff --git a/ci/constraints.txt b/ci/constraints.txt new file mode 100644 index 000000000..ea0efd596 --- /dev/null +++ b/ci/constraints.txt @@ -0,0 +1 @@ +numba>=0.56 diff --git a/ci/scripts/min-deps.py b/ci/scripts/min-deps.py index b792ad8a8..0d49d151e 100755 --- a/ci/scripts/min-deps.py +++ b/ci/scripts/min-deps.py @@ -11,6 +11,8 @@ import argparse import sys from collections import deque +from contextlib import ExitStack +from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING @@ -23,7 +25,9 @@ from packaging.version import Version if TYPE_CHECKING: - from collections.abc import Generator, Iterable + from collections.abc import Generator, Iterable, Sequence + from collections.abc import Set as AbstractSet + from typing import Any, Self def min_dep(req: Requirement) -> Requirement: @@ -75,34 +79,92 @@ def extract_min_deps( yield min_dep(req) -def main(): - 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`.""", - usage="pip install `python min-deps.py pyproject.toml`", - ) - parser.add_argument( - "path", type=Path, help="pyproject.toml to parse minimum dependencies from" - ) - parser.add_argument( - "--extras", type=str, nargs="*", default=(), help="extras to install" - ) - - args = parser.parse_args() - - pyproject = tomllib.loads(args.path.read_text()) +class Args(argparse.Namespace): + """\ + Parse a pyproject.toml file and output a list of minimum dependencies. + Output is optimized for `[uv] pip install` (see `-o`/`--output` for details). + """ - project_name = pyproject["project"]["name"] + _path: Path + output: Path | None + _extras: list[str] + _all_extras: bool + + @classmethod + def parse(cls, argv: Sequence[str] | None = None) -> Self: + return cls.parser().parse_args(argv, cls()) + + @classmethod + def parser(cls) -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="min-deps", + description=cls.__doc__, + usage="pip install `python min-deps.py pyproject.toml`", + ) + parser.add_argument( + "_path", + metavar="pyproject.toml", + type=Path, + help="Path to pyproject.toml to parse minimum dependencies from", + ) + parser.add_argument( + "--extras", + dest="_extras", + metavar="EXTRA", + type=str, + nargs="*", + default=(), + help="extras to install", + ) + parser.add_argument( + "--all-extras", + dest="_all_extras", + action="store_true", + help="get all extras", + ) + parser.add_argument( + *("--output", "-o"), + metavar="FILE", + 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`." + ), + ) + return parser + + @cached_property + def pyproject(self) -> dict[str, Any]: + return tomllib.loads(self._path.read_text()) + + @cached_property + def extras(self) -> AbstractSet[str]: + if self._extras: + if self._all_extras: + sys.exit("Cannot specify both --extras and --all-extras") + return dict.fromkeys(self._extras).keys() + if not self._all_extras: + return set() + return self.pyproject["project"]["optional-dependencies"].keys() + + +def main(argv: Sequence[str] | None = None) -> None: + args = Args.parse(argv) + + project_name = args.pyproject["project"]["name"] deps = [ - *map(Requirement, pyproject["project"]["dependencies"]), + *map(Requirement, args.pyproject["project"]["dependencies"]), *(Requirement(f"{project_name}[{extra}]") for extra in args.extras), ] - min_deps = extract_min_deps(deps, pyproject=pyproject) + min_deps = extract_min_deps(deps, pyproject=args.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 b49ba340e..beccdd26d 100755 --- a/ci/scripts/towncrier_automation.py +++ b/ci/scripts/towncrier_automation.py @@ -1,4 +1,7 @@ #!/usr/bin/env python3 +# /// script +# dependencies = [ "towncrier", "packaging" ] +# /// from __future__ import annotations import argparse diff --git a/hatch.toml b/hatch.toml index 738056567..12ada54da 100644 --- a/hatch.toml +++ b/hatch.toml @@ -4,21 +4,26 @@ features = ["dev"] [envs.docs] features = ["doc"] -extra-dependencies = ["setuptools"] # https://bitbucket.org/pybtex-devs/pybtex/issues/169 scripts.build = "sphinx-build -M html docs docs/_build -W --keep-going {args}" +scripts.open = "python3 -m webbrowser -t docs/_build/html/index.html" scripts.clean = "git clean -fdX -- {args:docs}" [envs.towncrier] +scripts.create = "towncrier create {args}" scripts.build = "python3 ci/scripts/towncrier_automation.py {args}" scripts.clean = "git restore --source=HEAD --staged --worktree -- docs/release-notes" [envs.hatch-test] default-args = [] -extra-dependencies = ["ipykernel"] features = ["dev", "test"] +extra-dependencies = ["ipykernel"] +env-vars.UV_CONSTRAINT = "ci/constraints.txt" overrides.matrix.deps.env-vars = [ - { key = "UV_PRERELEASE", value = "allow", if = ["pre"] }, - { key = "UV_RESOLUTION", value = "lowest-direct", if = ["min"] }, + { if = ["pre"], key = "UV_PRERELEASE", value = "allow" }, + { if = ["min"], key = "UV_CONSTRAINT", value = "ci/constraints.txt ci/min-deps.txt" }, +] +overrides.matrix.deps.pre-install-commands = [ + { if = ["min"], value = "uv run ci/scripts/min-deps.py pyproject.toml --all-extras -o ci/min-deps.txt" }, ] overrides.matrix.deps.python = [ { if = ["min"], value = "3.10" }, diff --git a/pyproject.toml b/pyproject.toml index eeff608b5..66e2815bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ dependencies = [ "numpy>=1.23", # https://github.com/scverse/anndata/issues/1434 "scipy >1.8", - "h5py>=3.6", + "h5py>=3.7", "exceptiongroup; python_version<'3.11'", "natsort", "packaging>=20.0", @@ -74,7 +74,7 @@ doc = [ "nbsphinx", "scanpydoc[theme,typehints] >=0.14.1", "zarr", - "awkward>=2.0.7", + "awkward>=2.3", "IPython", # For syntax highlighting in notebooks "myst_parser", "sphinx_design>=0.5.0", @@ -93,7 +93,7 @@ test = [ "openpyxl", "joblib", "boltons", - "scanpy", + "scanpy>=1.9.8", "httpx", # For data downloading "dask[distributed]", "awkward>=2.3",