1
1
import json
2
2
import logging
3
3
import os
4
- from dataclasses import dataclass , field
5
4
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
10
6
11
7
from pip ._internal .cli import cmdoptions
12
8
from pip ._internal .cli .cmdoptions import make_target_python
13
9
from pip ._internal .cli .req_command import RequirementCommand , with_cleanup
14
10
from pip ._internal .cli .status_codes import SUCCESS
15
11
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
18
12
from pip ._internal .req .req_tracker import get_requirement_tracker
19
13
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
28
15
from pip ._internal .utils .misc import ensure_dir , normalize_path , write_output
29
16
from pip ._internal .utils .temp_dir import TempDirectory
30
17
31
18
logger = logging .getLogger (__name__ )
32
19
33
20
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
-
89
21
class DownloadCommand (RequirementCommand ):
90
22
"""
91
23
Download packages from:
@@ -220,13 +152,6 @@ def run(self, options: Values, args: List[str]) -> int:
220
152
221
153
self .trace_basic_info (finder )
222
154
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.
230
155
requirement_set = resolver .resolve (reqs , check_supported_wheels = True )
231
156
232
157
if not options .dry_run :
@@ -239,6 +164,8 @@ def run(self, options: Values, args: List[str]) -> int:
239
164
if downloaded :
240
165
write_output ("Successfully downloaded %s" , " " .join (downloaded ))
241
166
167
+ # The rest of this method pertains to generating the ResolutionReport with
168
+ # --report.
242
169
if not options .json_report_file :
243
170
return SUCCESS
244
171
if not isinstance (requirement_set , RequirementSetWithCandidates ):
@@ -249,98 +176,13 @@ def run(self, options: Values, args: List[str]) -> int:
249
176
"so `pip download --report` cannot be used with it. "
250
177
)
251
178
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
273
181
)
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
- )
341
182
342
183
# Write a simplified representation of the resolution to stdout.
343
184
write_output (resolution_result .as_basic_log (options .json_report_file ))
185
+ # Write the full report data to the JSON output file.
344
186
with open (options .json_report_file , "w" ) as f :
345
187
json .dump (resolution_result .as_json (), f , indent = 4 )
346
188
0 commit comments