Skip to content

Commit 8c542cd

Browse files
committed
Implement Dependency Group option: --group
`--group` is supported on `download` and `install` commands. The option is parsed into the more verbose and explicit `dependency_groups` name on the parsed args. Both of these commands invoke the same processor for resolving dependency groups, which loads `pyproject.toml` and resolves the list of provided groups against the `[dependency-groups]` table. A small alteration is made to `pip wheel` to initialize `dependency_groups = []`, as this allows for some lower-level consistency in the handling of the commands.
1 parent fb55c1d commit 8c542cd

File tree

6 files changed

+95
-1
lines changed

6 files changed

+95
-1
lines changed

src/pip/_internal/cli/cmdoptions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,17 @@ def _handle_no_cache_dir(
733733
help="Don't install package dependencies.",
734734
)
735735

736+
dependency_groups: Callable[..., Option] = partial(
737+
Option,
738+
"--group",
739+
dest="dependency_groups",
740+
default=[],
741+
action="append",
742+
metavar="group",
743+
help="Install a named dependency-group from `pyproject.toml` "
744+
"in the current directory.",
745+
)
746+
736747
ignore_requires_python: Callable[..., Option] = partial(
737748
Option,
738749
"--ignore-requires-python",

src/pip/_internal/cli/req_command.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
install_req_from_parsed_requirement,
2929
install_req_from_req_string,
3030
)
31+
from pip._internal.req.req_dependency_group import parse_dependency_groups
3132
from pip._internal.req.req_file import parse_requirements
3233
from pip._internal.req.req_install import InstallRequirement
3334
from pip._internal.resolution.base import BaseResolver
@@ -240,6 +241,18 @@ def get_requirements(
240241
)
241242
requirements.append(req_to_add)
242243

244+
if options.dependency_groups:
245+
for req in parse_dependency_groups(
246+
options.dependency_groups, session, finder=finder, options=options
247+
):
248+
req_to_add = install_req_from_req_string(
249+
req,
250+
isolated=options.isolated_mode,
251+
use_pep517=options.use_pep517,
252+
user_supplied=True,
253+
)
254+
requirements.append(req_to_add)
255+
243256
for req in options.editables:
244257
req_to_add = install_req_from_editable(
245258
req,
@@ -272,7 +285,12 @@ def get_requirements(
272285
if any(req.has_hash_options for req in requirements):
273286
options.require_hashes = True
274287

275-
if not (args or options.editables or options.requirements):
288+
if not (
289+
args
290+
or options.editables
291+
or options.requirements
292+
or options.dependency_groups
293+
):
276294
opts = {"name": self.name}
277295
if options.find_links:
278296
raise CommandError(

src/pip/_internal/commands/download.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class DownloadCommand(RequirementCommand):
3838
def add_options(self) -> None:
3939
self.cmd_opts.add_option(cmdoptions.constraints())
4040
self.cmd_opts.add_option(cmdoptions.requirements())
41+
self.cmd_opts.add_option(cmdoptions.dependency_groups())
4142
self.cmd_opts.add_option(cmdoptions.no_deps())
4243
self.cmd_opts.add_option(cmdoptions.global_options())
4344
self.cmd_opts.add_option(cmdoptions.no_binary())

src/pip/_internal/commands/install.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def add_options(self) -> None:
8484
self.cmd_opts.add_option(cmdoptions.pre())
8585

8686
self.cmd_opts.add_option(cmdoptions.editable())
87+
self.cmd_opts.add_option(cmdoptions.dependency_groups())
8788
self.cmd_opts.add_option(
8889
"--dry-run",
8990
action="store_true",

src/pip/_internal/commands/wheel.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ def add_options(self) -> None:
102102

103103
@with_cleanup
104104
def run(self, options: Values, args: List[str]) -> int:
105+
# dependency-groups aren't desirable with `pip wheel`, but providing it
106+
# consistently allows RequirementCommand to expect it to be present
107+
options.dependency_groups = []
108+
105109
session = self.get_default_session(options)
106110

107111
finder = self._build_package_finder(options, session)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import optparse
2+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
3+
4+
from pip._vendor import tomli
5+
from pip._vendor.dependency_groups import resolve as resolve_dependency_group
6+
7+
from pip._internal.exceptions import InstallationError
8+
from pip._internal.network.session import PipSession
9+
10+
if TYPE_CHECKING:
11+
from pip._internal.index.package_finder import PackageFinder
12+
13+
14+
def parse_dependency_groups(
15+
groups: List[str],
16+
session: PipSession,
17+
finder: Optional["PackageFinder"] = None,
18+
options: Optional[optparse.Values] = None,
19+
) -> List[str]:
20+
"""
21+
Parse dependency groups data in a way which is sensitive to the `pip` context and
22+
raises InstallationErrors if anything goes wrong.
23+
"""
24+
pyproject = _load_pyproject()
25+
26+
if "dependency-groups" not in pyproject:
27+
raise InstallationError(
28+
"[dependency-groups] table was missing. Cannot resolve '--group' options."
29+
)
30+
raw_dependency_groups = pyproject["dependency-groups"]
31+
if not isinstance(raw_dependency_groups, dict):
32+
raise InstallationError(
33+
"[dependency-groups] table was malformed. Cannot resolve '--group' options."
34+
)
35+
36+
try:
37+
return list(resolve_dependency_group(raw_dependency_groups, *groups))
38+
except (ValueError, TypeError, LookupError) as e:
39+
raise InstallationError("[dependency-groups] resolution failed: {e}") from e
40+
41+
42+
def _load_pyproject() -> Dict[str, Any]:
43+
"""
44+
This helper loads pyproject.toml from the current working directory.
45+
46+
It does not allow specification of the path to be used and raises an
47+
InstallationError if the operation fails.
48+
"""
49+
try:
50+
with open("pyproject.toml", "rb") as fp:
51+
return tomli.load(fp)
52+
except FileNotFoundError:
53+
raise InstallationError(
54+
"pyproject.toml not found. Cannot resolve '--group' options."
55+
)
56+
except tomli.TOMLDecodeError as e:
57+
raise InstallationError(f"Error parsing pyproject.toml: {e}") from e
58+
except OSError as e:
59+
raise InstallationError(f"Error reading pyproject.toml: {e}") from e

0 commit comments

Comments
 (0)