Skip to content

Commit c78bb3d

Browse files
move the resolution data printing to a new resolve command
1 parent 4e2a593 commit c78bb3d

File tree

3 files changed

+236
-90
lines changed

3 files changed

+236
-90
lines changed

Diff for: src/pip/_internal/commands/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
"DownloadCommand",
2929
"Download packages.",
3030
),
31+
"resolve": CommandInfo(
32+
"pip._internal.commands.resolve",
33+
"ResolveCommand",
34+
"Resolve and print out package dependencies and metadata.",
35+
),
3136
"uninstall": CommandInfo(
3237
"pip._internal.commands.uninstall",
3338
"UninstallCommand",

Diff for: src/pip/_internal/commands/download.py

+2-90
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,19 @@
1-
import json
21
import logging
32
import os
4-
from dataclasses import dataclass, field
53
from optparse import Values
6-
from typing import Any, Dict, List
7-
8-
from pip._vendor.packaging.requirements import Requirement
4+
from typing import List
95

106
from pip._internal.cli import cmdoptions
117
from pip._internal.cli.cmdoptions import make_target_python
128
from pip._internal.cli.req_command import RequirementCommand, with_cleanup
139
from pip._internal.cli.status_codes import SUCCESS
14-
from pip._internal.models.link import RequirementDownloadInfo
1510
from pip._internal.req.req_tracker import get_requirement_tracker
16-
from pip._internal.resolution.base import RequirementSetWithCandidates
1711
from pip._internal.utils.misc import ensure_dir, normalize_path, write_output
1812
from pip._internal.utils.temp_dir import TempDirectory
1913

2014
logger = logging.getLogger(__name__)
2115

2216

23-
@dataclass
24-
class DownloadInfos:
25-
implicit_requirements: List[Requirement] = field(default_factory=list)
26-
resolution: Dict[str, RequirementDownloadInfo] = field(default_factory=dict)
27-
28-
def as_json(self) -> Dict[str, Any]:
29-
return {
30-
"implicit_requirements": [str(req) for req in self.implicit_requirements],
31-
"resolution": {
32-
name: info.as_json() for name, info in self.resolution.items()
33-
},
34-
}
35-
36-
3717
class DownloadCommand(RequirementCommand):
3818
"""
3919
Download packages from:
@@ -82,25 +62,6 @@ def add_options(self) -> None:
8262
help="Download packages into <dir>.",
8363
)
8464

85-
self.cmd_opts.add_option(
86-
"--print-download-urls",
87-
dest="print_download_urls",
88-
metavar="output-file",
89-
default=None,
90-
help=("Print URLs of any downloaded distributions to this file."),
91-
)
92-
93-
self.cmd_opts.add_option(
94-
"--avoid-wheel-downloads",
95-
dest="avoid_wheel_downloads",
96-
default=False,
97-
action="store_true",
98-
help=(
99-
"Where possible, avoid downloading wheels. This is "
100-
"currently only useful if --print-download-urls is set."
101-
),
102-
)
103-
10465
cmdoptions.add_target_python_options(self.cmd_opts)
10566

10667
index_opts = cmdoptions.make_option_group(
@@ -160,7 +121,6 @@ def run(self, options: Values, args: List[str]) -> int:
160121
options=options,
161122
ignore_requires_python=options.ignore_requires_python,
162123
py_version_info=options.python_version,
163-
avoid_wheel_downloads=options.avoid_wheel_downloads,
164124
)
165125

166126
self.trace_basic_info(finder)
@@ -169,59 +129,11 @@ def run(self, options: Values, args: List[str]) -> int:
169129

170130
downloaded: List[str] = []
171131
for req in requirement_set.requirements.values():
172-
# If this distribution was not already satisfied, that means we
173-
# downloaded it.
174132
if req.satisfied_by is None:
175-
preparer.save_linked_requirement(req)
176133
assert req.name is not None
134+
preparer.save_linked_requirement(req)
177135
downloaded.append(req.name)
178-
179-
download_infos = DownloadInfos()
180-
if options.print_download_urls:
181-
if isinstance(requirement_set, RequirementSetWithCandidates):
182-
for candidate in requirement_set.candidates.mapping.values():
183-
# This will occur for the python version requirement, for example.
184-
if candidate.name not in requirement_set.requirements:
185-
assert (
186-
tuple(candidate.iter_dependencies(with_requires=True)) == ()
187-
)
188-
download_infos.implicit_requirements.append(
189-
candidate.as_serializable_requirement()
190-
)
191-
continue
192-
req = requirement_set.requirements[candidate.name]
193-
assert req.name is not None
194-
assert req.link is not None
195-
assert req.name not in download_infos.resolution
196-
197-
dependencies: List[Requirement] = []
198-
for maybe_dep in candidate.iter_dependencies(with_requires=True):
199-
if maybe_dep is None:
200-
continue
201-
maybe_req = maybe_dep.as_serializable_requirement()
202-
if maybe_req is None:
203-
continue
204-
dependencies.append(maybe_req)
205-
206-
download_infos.resolution[
207-
req.name
208-
] = RequirementDownloadInfo.from_req_and_link_and_deps(
209-
req=candidate.as_serializable_requirement(),
210-
dependencies=dependencies,
211-
link=req.link,
212-
)
213-
else:
214-
logger.warning(
215-
"--print-download-urls is being used with the legacy resolver. "
216-
"The legacy resolver does not retain detailed dependency "
217-
"information, so all the fields in the output JSON file "
218-
"will be empty."
219-
)
220-
221136
if downloaded:
222137
write_output("Successfully downloaded %s", " ".join(downloaded))
223-
if options.print_download_urls:
224-
with open(options.print_download_urls, "w") as f:
225-
json.dump(download_infos.as_json(), f, indent=4)
226138

227139
return SUCCESS

Diff for: src/pip/_internal/commands/resolve.py

+229
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import json
2+
import logging
3+
import os
4+
from dataclasses import dataclass, field
5+
from optparse import Values
6+
from typing import Any, Dict, List
7+
8+
from pip._vendor.packaging.requirements import Requirement
9+
10+
from pip._internal.cli import cmdoptions
11+
from pip._internal.cli.cmdoptions import make_target_python
12+
from pip._internal.cli.req_command import RequirementCommand, with_cleanup
13+
from pip._internal.cli.status_codes import SUCCESS
14+
from pip._internal.exceptions import CommandError
15+
from pip._internal.models.link import RequirementDownloadInfo
16+
from pip._internal.req.req_tracker import get_requirement_tracker
17+
from pip._internal.resolution.base import RequirementSetWithCandidates
18+
from pip._internal.utils.misc import ensure_dir, normalize_path, write_output
19+
from pip._internal.utils.temp_dir import TempDirectory
20+
21+
logger = logging.getLogger(__name__)
22+
23+
24+
@dataclass
25+
class DownloadInfos:
26+
implicit_requirements: List[Requirement] = field(default_factory=list)
27+
resolution: Dict[str, RequirementDownloadInfo] = field(default_factory=dict)
28+
29+
def as_basic_log(self) -> str:
30+
implicits = ", ".join(f"'{req}'" for req in self.implicit_requirements)
31+
resolved = "\n".join(
32+
f"{info.req}: {info.url}" for info in self.resolution.values()
33+
)
34+
return "\n".join(
35+
[
36+
f"Implicit requirements: {implicits}",
37+
"Resolution:",
38+
f"{resolved}",
39+
]
40+
)
41+
42+
def as_json(self) -> Dict[str, Any]:
43+
return {
44+
"implicit_requirements": [str(req) for req in self.implicit_requirements],
45+
"resolution": {
46+
name: info.as_json() for name, info in self.resolution.items()
47+
},
48+
}
49+
50+
51+
class ResolveCommand(RequirementCommand):
52+
"""
53+
Download packages from:
54+
55+
- PyPI (and other indexes) using requirement specifiers.
56+
- VCS project urls.
57+
- Local project directories.
58+
- Local or remote source archives.
59+
60+
pip also supports downloading from "requirements files", which provide
61+
an easy way to specify a whole environment to be downloaded.
62+
"""
63+
64+
usage = """
65+
%prog [options] <requirement specifier> [package-index-options] ...
66+
%prog [options] -r <requirements file> [package-index-options] ...
67+
%prog [options] <vcs project url> ...
68+
%prog [options] <local project path> ...
69+
%prog [options] <archive url/path> ..."""
70+
71+
def add_options(self) -> None:
72+
self.cmd_opts.add_option(cmdoptions.constraints())
73+
self.cmd_opts.add_option(cmdoptions.requirements())
74+
self.cmd_opts.add_option(cmdoptions.no_deps())
75+
self.cmd_opts.add_option(cmdoptions.global_options())
76+
self.cmd_opts.add_option(cmdoptions.no_binary())
77+
self.cmd_opts.add_option(cmdoptions.only_binary())
78+
self.cmd_opts.add_option(cmdoptions.prefer_binary())
79+
self.cmd_opts.add_option(cmdoptions.src())
80+
self.cmd_opts.add_option(cmdoptions.pre())
81+
self.cmd_opts.add_option(cmdoptions.require_hashes())
82+
self.cmd_opts.add_option(cmdoptions.progress_bar())
83+
self.cmd_opts.add_option(cmdoptions.no_build_isolation())
84+
self.cmd_opts.add_option(cmdoptions.use_pep517())
85+
self.cmd_opts.add_option(cmdoptions.no_use_pep517())
86+
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
87+
88+
self.cmd_opts.add_option(
89+
"-d",
90+
"--dest",
91+
"--destination-dir",
92+
"--destination-directory",
93+
dest="download_dir",
94+
metavar="dir",
95+
default=os.curdir,
96+
help="Download packages into <dir>.",
97+
)
98+
99+
self.cmd_opts.add_option(
100+
"-o",
101+
"--json",
102+
"--json-output",
103+
"--json-output-file",
104+
dest="json_output_file",
105+
metavar="file",
106+
help="Print a JSON object representing the resolve into <file>.",
107+
)
108+
109+
cmdoptions.add_target_python_options(self.cmd_opts)
110+
111+
index_opts = cmdoptions.make_option_group(
112+
cmdoptions.index_group,
113+
self.parser,
114+
)
115+
116+
self.parser.insert_option_group(0, index_opts)
117+
self.parser.insert_option_group(0, self.cmd_opts)
118+
119+
@with_cleanup
120+
def run(self, options: Values, args: List[str]) -> int:
121+
122+
options.ignore_installed = True
123+
# editable doesn't really make sense for `pip download`, but the bowels
124+
# of the RequirementSet code require that property.
125+
options.editables = []
126+
127+
cmdoptions.check_dist_restriction(options)
128+
129+
options.download_dir = normalize_path(options.download_dir)
130+
ensure_dir(options.download_dir)
131+
132+
session = self.get_default_session(options)
133+
134+
target_python = make_target_python(options)
135+
finder = self._build_package_finder(
136+
options=options,
137+
session=session,
138+
target_python=target_python,
139+
ignore_requires_python=options.ignore_requires_python,
140+
)
141+
142+
req_tracker = self.enter_context(get_requirement_tracker())
143+
144+
directory = TempDirectory(
145+
delete=not options.no_clean,
146+
kind="download",
147+
globally_managed=True,
148+
)
149+
150+
reqs = self.get_requirements(args, options, finder, session)
151+
152+
preparer = self.make_requirement_preparer(
153+
temp_build_dir=directory,
154+
options=options,
155+
req_tracker=req_tracker,
156+
session=session,
157+
finder=finder,
158+
download_dir=options.download_dir,
159+
use_user_site=False,
160+
)
161+
162+
resolver = self.make_resolver(
163+
preparer=preparer,
164+
finder=finder,
165+
options=options,
166+
ignore_requires_python=options.ignore_requires_python,
167+
py_version_info=options.python_version,
168+
avoid_wheel_downloads=True,
169+
)
170+
171+
self.trace_basic_info(finder)
172+
173+
requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
174+
175+
downloaded: List[str] = []
176+
for req in requirement_set.requirements.values():
177+
# If this distribution was not already satisfied, that means we
178+
# downloaded it while executing this command.
179+
if req.satisfied_by is None:
180+
preparer.save_linked_requirement(req)
181+
assert req.name is not None
182+
downloaded.append(req.name)
183+
184+
download_infos = DownloadInfos()
185+
if not isinstance(requirement_set, RequirementSetWithCandidates):
186+
raise CommandError(
187+
"The legacy resolver is being used via "
188+
"--use-deprecated=legacy-resolver."
189+
"The legacy resolver does not retain detailed dependency information, "
190+
"so `pip resolve` cannot be used with it. "
191+
)
192+
for candidate in requirement_set.candidates.mapping.values():
193+
# This will occur for the python version requirement, for example.
194+
if candidate.name not in requirement_set.requirements:
195+
assert tuple(candidate.iter_dependencies(with_requires=True)) == ()
196+
download_infos.implicit_requirements.append(
197+
candidate.as_serializable_requirement()
198+
)
199+
continue
200+
req = requirement_set.requirements[candidate.name]
201+
assert req.name is not None
202+
assert req.link is not None
203+
assert req.name not in download_infos.resolution
204+
205+
dependencies: List[Requirement] = []
206+
for maybe_dep in candidate.iter_dependencies(with_requires=True):
207+
if maybe_dep is None:
208+
continue
209+
maybe_req = maybe_dep.as_serializable_requirement()
210+
if maybe_req is None:
211+
continue
212+
dependencies.append(maybe_req)
213+
214+
download_infos.resolution[
215+
req.name
216+
] = RequirementDownloadInfo.from_req_and_link_and_deps(
217+
req=candidate.as_serializable_requirement(),
218+
dependencies=dependencies,
219+
link=req.link,
220+
)
221+
222+
if downloaded:
223+
write_output("Successfully downloaded %s", " ".join(downloaded))
224+
write_output(download_infos.as_basic_log())
225+
if options.json_output_file:
226+
with open(options.json_output_file, "w") as f:
227+
json.dump(download_infos.as_json(), f, indent=4)
228+
229+
return SUCCESS

0 commit comments

Comments
 (0)