Skip to content

Commit a67c59f

Browse files
move the --report implementation into resolvelib
1 parent b2abe6a commit a67c59f

File tree

3 files changed

+216
-166
lines changed

3 files changed

+216
-166
lines changed

src/pip/_internal/commands/download.py

+7-165
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,23 @@
11
import json
22
import logging
33
import os
4-
from dataclasses import dataclass, field
54
from optparse import Values
6-
from typing import Any, Dict, List, Optional, Tuple
7-
8-
from pip._vendor.packaging.requirements import Requirement
9-
from pip._vendor.packaging.specifiers import SpecifierSet
5+
from typing import List
106

117
from pip._internal.cli import cmdoptions
128
from pip._internal.cli.cmdoptions import make_target_python
139
from pip._internal.cli.req_command import RequirementCommand, with_cleanup
1410
from pip._internal.cli.status_codes import SUCCESS
1511
from pip._internal.exceptions import CommandError
16-
from pip._internal.models.link import LinkWithSource, URLDownloadInfo
17-
from pip._internal.req.req_install import produce_exact_version_specifier
1812
from pip._internal.req.req_tracker import get_requirement_tracker
1913
from pip._internal.resolution.base import RequirementSetWithCandidates
20-
from pip._internal.resolution.resolvelib.candidates import (
21-
LinkCandidate,
22-
RequiresPythonCandidate,
23-
)
24-
from pip._internal.resolution.resolvelib.requirements import (
25-
ExplicitRequirement,
26-
RequiresPythonRequirement,
27-
)
14+
from pip._internal.resolution.resolvelib.reporter import ResolutionResult
2815
from pip._internal.utils.misc import ensure_dir, normalize_path, write_output
2916
from pip._internal.utils.temp_dir import TempDirectory
3017

3118
logger = logging.getLogger(__name__)
3219

3320

34-
@dataclass(frozen=True)
35-
class ResolvedCandidate:
36-
"""Coalesce all the information pip's resolver retains about an
37-
installation candidate."""
38-
39-
req: Requirement
40-
download_info: URLDownloadInfo
41-
dependencies: Tuple[Requirement, ...]
42-
requires_python: Optional[SpecifierSet]
43-
44-
def as_json(self) -> Dict[str, Any]:
45-
"""Return a JSON-serializable representation of this install candidate."""
46-
return {
47-
"requirement": str(self.req),
48-
"download_info": self.download_info.as_json(),
49-
"dependencies": {dep.name: str(dep) for dep in self.dependencies},
50-
"requires_python": str(self.requires_python)
51-
if self.requires_python
52-
else None,
53-
}
54-
55-
56-
@dataclass
57-
class ResolutionResult:
58-
"""The inputs and outputs of a pip internal resolve process."""
59-
60-
input_requirements: Tuple[str, ...]
61-
python_version: Optional[SpecifierSet] = None
62-
candidates: Dict[str, ResolvedCandidate] = field(default_factory=dict)
63-
64-
def as_basic_log(self, output_json_path: str) -> str:
65-
"""Generate a summary of the detailed JSON report produced with --report."""
66-
inputs = " ".join(f"'{req}'" for req in self.input_requirements)
67-
resolved = " ".join(f"'{info.req}'" for info in self.candidates.values())
68-
return "\n".join(
69-
[
70-
f"Python version: '{self.python_version}'",
71-
f"Input requirements: {inputs}",
72-
f"Resolution: {resolved}",
73-
f"JSON report written to '{output_json_path}'.",
74-
]
75-
)
76-
77-
def as_json(self) -> Dict[str, Any]:
78-
"""Return a JSON-serializable representation of the resolve process."""
79-
return {
80-
"experimental": True,
81-
"input_requirements": [str(req) for req in self.input_requirements],
82-
"python_version": str(self.python_version),
83-
"candidates": {
84-
name: info.as_json() for name, info in self.candidates.items()
85-
},
86-
}
87-
88-
8921
class DownloadCommand(RequirementCommand):
9022
"""
9123
Download packages from:
@@ -220,13 +152,6 @@ def run(self, options: Values, args: List[str]) -> int:
220152

221153
self.trace_basic_info(finder)
222154

223-
# TODO: for performance, try to decouple extracting sdist metadata from
224-
# actually building the sdist. See https://github.com/pypa/pip/issues/8929.
225-
# As mentioned in that issue, PEP 658 support on PyPI would address many cases,
226-
# but it would drastically improve performance for many existing packages if we
227-
# attempted to extract PKG-INFO or .egg-info from non-wheel files, falling back
228-
# to the slower setup.py invocation if not found. LazyZipOverHTTP and
229-
# MemoryWheel already implement such a hack for wheel files specifically.
230155
requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
231156

232157
if not options.dry_run:
@@ -239,6 +164,8 @@ def run(self, options: Values, args: List[str]) -> int:
239164
if downloaded:
240165
write_output("Successfully downloaded %s", " ".join(downloaded))
241166

167+
# The rest of this method pertains to generating the ResolutionReport with
168+
# --report.
242169
if not options.json_report_file:
243170
return SUCCESS
244171
if not isinstance(requirement_set, RequirementSetWithCandidates):
@@ -249,98 +176,13 @@ def run(self, options: Values, args: List[str]) -> int:
249176
"so `pip download --report` cannot be used with it. "
250177
)
251178

252-
# Reconstruct the input requirements provided to the resolve.
253-
input_requirements: List[str] = []
254-
for ireq in reqs:
255-
if ireq.req:
256-
# If the initial requirement string contained a url (retained in
257-
# InstallRequirement.link), add it back to the requirement string
258-
# included in the JSON report.
259-
if ireq.link:
260-
req_string = f"{ireq.req}@{ireq.link.url}"
261-
else:
262-
req_string = str(ireq.req)
263-
else:
264-
assert ireq.link
265-
req_string = ireq.link.url
266-
267-
input_requirements.append(req_string)
268-
269-
# Scan all the elements of the resulting `RequirementSet` and map it back to all
270-
# the install candidates preserved by `RequirementSetWithCandidates`.
271-
resolution_result = ResolutionResult(
272-
input_requirements=tuple(input_requirements)
179+
resolution_result = ResolutionResult.generate_resolve_report(
180+
reqs, requirement_set
273181
)
274-
for candidate in requirement_set.candidates.mapping.values():
275-
# This will occur for the python version requirement, for example.
276-
if candidate.name not in requirement_set.requirements:
277-
if isinstance(candidate, RequiresPythonCandidate):
278-
assert resolution_result.python_version is None
279-
resolution_result.python_version = produce_exact_version_specifier(
280-
str(candidate.version)
281-
)
282-
continue
283-
raise TypeError(
284-
f"unknown candidate not found in requirement set: {candidate}"
285-
)
286-
287-
req = requirement_set.requirements[candidate.name]
288-
assert req.name is not None
289-
assert req.link is not None
290-
assert req.name not in resolution_result.candidates
291-
292-
# Scan the dependencies of the installation candidates, which cover both
293-
# normal dependencies as well as Requires-Python information.
294-
requires_python: Optional[SpecifierSet] = None
295-
dependencies: List[Requirement] = []
296-
for maybe_dep in candidate.iter_dependencies(with_requires=True):
297-
# It's unclear why `.iter_dependencies()` may occasionally yield `None`.
298-
if maybe_dep is None:
299-
continue
300-
# There will only ever be one of these for each candidate, if any. We
301-
# extract the version specifier.
302-
if isinstance(maybe_dep, RequiresPythonRequirement):
303-
requires_python = maybe_dep.specifier
304-
continue
305-
306-
# Convert the 2020 resolver-internal Requirement subclass instance into
307-
# a `packaging.requirements.Requirement` instance.
308-
maybe_req = maybe_dep.as_serializable_requirement()
309-
if maybe_req is None:
310-
continue
311-
312-
# For `ExplicitRequirement`s only, we want to make sure we propagate any
313-
# source URL into a dependency's `packaging.requirements.Requirement`
314-
# instance.
315-
if isinstance(maybe_dep, ExplicitRequirement):
316-
dep_candidate = maybe_dep.candidate
317-
if maybe_req.url is None and isinstance(
318-
dep_candidate, LinkCandidate
319-
):
320-
assert dep_candidate.source_link is not None
321-
maybe_req = Requirement(
322-
f"{maybe_req}@{dep_candidate.source_link.url}"
323-
)
324-
325-
dependencies.append(maybe_req)
326-
327-
# Mutate the candidates dictionary to add this candidate after processing
328-
# any dependencies and python version requirement.
329-
resolution_result.candidates[req.name] = ResolvedCandidate(
330-
req=candidate.as_serializable_requirement(),
331-
download_info=URLDownloadInfo.from_link_with_source(
332-
LinkWithSource(
333-
req.link,
334-
source_dir=req.source_dir,
335-
link_is_in_wheel_cache=req.original_link_is_in_wheel_cache,
336-
)
337-
),
338-
dependencies=tuple(dependencies),
339-
requires_python=requires_python,
340-
)
341182

342183
# Write a simplified representation of the resolution to stdout.
343184
write_output(resolution_result.as_basic_log(options.json_report_file))
185+
# Write the full report data to the JSON output file.
344186
with open(options.json_report_file, "w") as f:
345187
json.dump(resolution_result.as_json(), f, indent=4)
346188

0 commit comments

Comments
 (0)