Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Constrain all extras for min-deps job #3367

Merged
merged 1 commit into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 74 additions & 33 deletions ci/scripts/min-deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion hatch.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
Loading