11from  __future__ import  annotations 
22
33import  io 
4+ import  json 
45import  os 
56import  re 
67import  sys 
@@ -79,6 +80,7 @@ def __init__(
7980        dst_file : BinaryIO ,
8081        click_ctx : Context ,
8182        dry_run : bool ,
83+         json_output : bool ,
8284        emit_header : bool ,
8385        emit_index_url : bool ,
8486        emit_trusted_host : bool ,
@@ -99,6 +101,7 @@ def __init__(
99101        self .dst_file  =  dst_file 
100102        self .click_ctx  =  click_ctx 
101103        self .dry_run  =  dry_run 
104+         self .json_output  =  json_output 
102105        self .emit_header  =  emit_header 
103106        self .emit_index_url  =  emit_index_url 
104107        self .emit_trusted_host  =  emit_trusted_host 
@@ -173,14 +176,61 @@ def write_flags(self) -> Iterator[str]:
173176        if  emitted :
174177            yield  "" 
175178
176-     def  _iter_lines (
179+     def  _get_json (
180+         self ,
181+         ireq : InstallRequirement ,
182+         line : str ,
183+         hashes : dict [InstallRequirement , set [str ]] |  None  =  None ,
184+         unsafe : bool  =  False ,
185+     ) ->  dict [str , str ]:
186+         """Get a JSON representation for an ``InstallRequirement``.""" 
187+         output_hashes  =  []
188+         if  hashes :
189+             ireq_hashes  =  hashes .get (ireq )
190+             if  ireq_hashes :
191+                 assert  isinstance (ireq_hashes , set )
192+                 output_hashes  =  list (ireq_hashes )
193+         hashable  =  True 
194+         if  ireq .link :
195+             if  ireq .link .is_vcs  or  (ireq .link .is_file  and  ireq .link .is_existing_dir ()):
196+                 hashable  =  False 
197+         markers  =  "" 
198+         if  ireq .markers :
199+             markers  =  str (ireq .markers )
200+         # Retrieve parent requirements from constructed line 
201+         splitted_line  =  [m .strip () for  m  in  unstyle (line ).split ("#" )]
202+         try :
203+             via  =  splitted_line [splitted_line .index ("via" ) +  1  :]
204+         except  ValueError :
205+             via  =  [splitted_line [- 1 ][len ("via " ) :]]
206+             if  via [0 ].startswith ("-r" ):
207+                 req_files  =  re .split (r"\s|," , via [0 ])
208+                 del  req_files [0 ]
209+                 via  =  ["-r" ]
210+                 for  req_file  in  req_files :
211+                     via .append (os .path .abspath (req_file ))
212+         ireq_json  =  {
213+             "name" : ireq .name ,
214+             "version" : str (ireq .specifier ).lstrip ("==" ),
215+             "requirement" : str (ireq .req ),
216+             "via" : via ,
217+             "line" : unstyle (line ),
218+             "hashable" : hashable ,
219+             "editable" : ireq .editable ,
220+             "hashes" : output_hashes ,
221+             "markers" : markers ,
222+             "unsafe" : unsafe ,
223+         }
224+         return  ireq_json 
225+ 
226+     def  _iter_ireqs (
177227        self ,
178228        results : set [InstallRequirement ],
179229        unsafe_requirements : set [InstallRequirement ],
180230        unsafe_packages : set [str ],
181231        markers : dict [str , Marker ],
182232        hashes : dict [InstallRequirement , set [str ]] |  None  =  None ,
183-     ) ->  Iterator [str ]:
233+     ) ->  Iterator [str ,  dict [ str ,  str ] ]:
184234        # default values 
185235        unsafe_packages  =  unsafe_packages  if  self .allow_unsafe  else  set ()
186236        hashes  =  hashes  or  {}
@@ -191,12 +241,11 @@ def _iter_lines(
191241        has_hashes  =  hashes  and  any (hash  for  hash  in  hashes .values ())
192242
193243        yielded  =  False 
194- 
195244        for  line  in  self .write_header ():
196-             yield  line 
245+             yield  line , {} 
197246            yielded  =  True 
198247        for  line  in  self .write_flags ():
199-             yield  line 
248+             yield  line , {} 
200249            yielded  =  True 
201250
202251        unsafe_requirements  =  unsafe_requirements  or  {
@@ -207,36 +256,36 @@ def _iter_lines(
207256        if  packages :
208257            for  ireq  in  sorted (packages , key = self ._sort_key ):
209258                if  has_hashes  and  not  hashes .get (ireq ):
210-                     yield  MESSAGE_UNHASHED_PACKAGE 
259+                     yield  MESSAGE_UNHASHED_PACKAGE , {} 
211260                    warn_uninstallable  =  True 
212261                line  =  self ._format_requirement (
213262                    ireq , markers .get (key_from_ireq (ireq )), hashes = hashes 
214263                )
215-                 yield  line 
264+                 yield  line ,  self . _get_json ( ireq ,  line ,  hashes = hashes ) 
216265            yielded  =  True 
217266
218267        if  unsafe_requirements :
219-             yield  "" 
268+             yield  "" , {} 
220269            yielded  =  True 
221270            if  has_hashes  and  not  self .allow_unsafe :
222-                 yield  MESSAGE_UNSAFE_PACKAGES_UNPINNED 
271+                 yield  MESSAGE_UNSAFE_PACKAGES_UNPINNED , {} 
223272                warn_uninstallable  =  True 
224273            else :
225-                 yield  MESSAGE_UNSAFE_PACKAGES 
274+                 yield  MESSAGE_UNSAFE_PACKAGES , {} 
226275
227276            for  ireq  in  sorted (unsafe_requirements , key = self ._sort_key ):
228277                ireq_key  =  key_from_ireq (ireq )
229278                if  not  self .allow_unsafe :
230-                     yield  comment (f"# { ireq_key }  )
279+                     yield  comment (f"# { ireq_key }  ), {} 
231280                else :
232281                    line  =  self ._format_requirement (
233282                        ireq , marker = markers .get (ireq_key ), hashes = hashes 
234283                    )
235-                     yield  line 
284+                     yield  line ,  self . _get_json ( ireq ,  line ,  unsafe = True ) 
236285
237286        # Yield even when there's no real content, so that blank files are written 
238287        if  not  yielded :
239-             yield  "" 
288+             yield  "" , {} 
240289
241290        if  warn_uninstallable :
242291            log .warning (MESSAGE_UNINSTALLABLE )
@@ -249,27 +298,33 @@ def write(
249298        markers : dict [str , Marker ],
250299        hashes : dict [InstallRequirement , set [str ]] |  None ,
251300    ) ->  None :
252-         if  not  self .dry_run :
301+         output_structure  =  []
302+         if  not  self .dry_run  or  self .json_output :
253303            dst_file  =  io .TextIOWrapper (
254304                self .dst_file ,
255305                encoding = "utf8" ,
256306                newline = self .linesep ,
257307                line_buffering = True ,
258308            )
259309        try :
260-             for  line   in  self ._iter_lines (
310+             for  line ,  ireq   in  self ._iter_ireqs (
261311                results , unsafe_requirements , unsafe_packages , markers , hashes 
262312            ):
263313                if  self .dry_run :
264314                    # Bypass the log level to always print this during a dry run 
265315                    log .log (line )
266316                else :
267-                     log .info (line )
317+                     if  not  self .json_output :
318+                         log .info (line )
268319                    dst_file .write (unstyle (line ))
269320                    dst_file .write ("\n " )
321+                 if  self .json_output  and  ireq :
322+                     output_structure .append (ireq )
270323        finally :
271-             if  not  self .dry_run :
324+             if  not  self .dry_run   or   self . json_output :
272325                dst_file .detach ()
326+             if  self .json_output :
327+                 print (json .dumps (output_structure , indent = 4 ))
273328
274329    def  _format_requirement (
275330        self ,
0 commit comments