From d3a2d51d934292f6a859347480938d0ad0744f9e Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Thu, 14 Nov 2024 11:35:50 +0100 Subject: [PATCH] Constrain all extras for min-deps job --- ci/scripts/min-deps.py | 107 ++++++++++++++++++++++++++++------------- hatch.toml | 2 +- 2 files changed, 75 insertions(+), 34 deletions(-) diff --git a/ci/scripts/min-deps.py b/ci/scripts/min-deps.py index b996302c0..b39fec2b4 100755 --- a/ci/scripts/min-deps.py +++ b/ci/scripts/min-deps.py @@ -12,6 +12,7 @@ import sys from collections import deque from contextlib import ExitStack +from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING @@ -25,6 +26,8 @@ if TYPE_CHECKING: 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: @@ -77,48 +80,86 @@ def extract_min_deps( class Args(argparse.Namespace): - path: Path + """\ + Parse a pyproject.toml file and output a list of minimum dependencies. + Output is optimized for `[uv] pip install` (see `-o`/`--output` for details). + """ + + _path: Path output: Path | None - extras: list[str] + _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: - parser = argparse.ArgumentParser( - prog="min-deps", - 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( - "path", type=Path, help="pyproject.toml to parse minimum dependencies from" - ) - 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(argv, Args()) - - pyproject = tomllib.loads(args.path.read_text()) + args = Args.parse(argv) - project_name = pyproject["project"]["name"] + 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) sep = "\n" if args.output else " " with ExitStack() as stack: diff --git a/hatch.toml b/hatch.toml index ad5db6097..3163d5d82 100644 --- a/hatch.toml +++ b/hatch.toml @@ -22,7 +22,7 @@ overrides.matrix.deps.env-vars = [ { 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" }, + { if = ["min"], value = "uv run ci/scripts/min-deps.py pyproject.toml --all-extras -o ci/scanpy-min-deps.txt" }, ] overrides.matrix.deps.python = [ { if = ["min"], value = "3.10" },