11import  json 
22import  logging 
33import  os 
4- from  dataclasses  import  dataclass , field 
54from  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
117from  pip ._internal .cli  import  cmdoptions 
128from  pip ._internal .cli .cmdoptions  import  make_target_python 
139from  pip ._internal .cli .req_command  import  RequirementCommand , with_cleanup 
1410from  pip ._internal .cli .status_codes  import  SUCCESS 
1511from  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 
1812from  pip ._internal .req .req_tracker  import  get_requirement_tracker 
1913from  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 
2815from  pip ._internal .utils .misc  import  ensure_dir , normalize_path , write_output 
2916from  pip ._internal .utils .temp_dir  import  TempDirectory 
3017
3118logger  =  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- 
8921class  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