Skip to content

Commit 3765b8d

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

File tree

3 files changed

+234
-90
lines changed

3 files changed

+234
-90
lines changed

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",

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

src/pip/_internal/commands/resolve.py

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

0 commit comments

Comments
 (0)