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,62 @@ 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+ if hashes :
188+ ireq_hashes = hashes .get (ireq )
189+ if ireq_hashes :
190+ assert isinstance (ireq_hashes , set )
191+ output_hashes = list (ireq_hashes )
192+ else :
193+ output_hashes = []
194+ hashable = True
195+ if ireq .link :
196+ if ireq .link .is_vcs or (ireq .link .is_file and ireq .link .is_existing_dir ()):
197+ hashable = False
198+ markers = ""
199+ if ireq .markers :
200+ markers = str (ireq .markers )
201+ # Retrieve parent requirements from constructed line
202+ splitted_line = [m .strip () for m in unstyle (line ).split ("#" )]
203+ try :
204+ via = splitted_line [splitted_line .index ("via" ) + 1 :]
205+ except ValueError :
206+ via = [splitted_line [- 1 ][len ("via " ) :]]
207+ if via [0 ].startswith ("-r" ):
208+ req_files = re .split (r"\s|," , via [0 ])
209+ del req_files [0 ]
210+ via = ["-r" ]
211+ for req_file in req_files :
212+ via .append (os .path .abspath (req_file ))
213+ ireq_json = {
214+ "name" : ireq .name ,
215+ "version" : str (ireq .specifier ).lstrip ("==" ),
216+ "requirement" : str (ireq .req ),
217+ "via" : via ,
218+ "line" : unstyle (line ),
219+ "hashable" : hashable ,
220+ "editable" : ireq .editable ,
221+ "hashes" : output_hashes ,
222+ "markers" : markers ,
223+ "unsafe" : unsafe ,
224+ }
225+ return ireq_json
226+
227+ def _iter_ireqs (
177228 self ,
178229 results : set [InstallRequirement ],
179230 unsafe_requirements : set [InstallRequirement ],
180231 unsafe_packages : set [str ],
181232 markers : dict [str , Marker ],
182233 hashes : dict [InstallRequirement , set [str ]] | None = None ,
183- ) -> Iterator [str ]:
234+ ) -> Iterator [str , dict [ str , str ] ]:
184235 # default values
185236 unsafe_packages = unsafe_packages if self .allow_unsafe else set ()
186237 hashes = hashes or {}
@@ -191,12 +242,11 @@ def _iter_lines(
191242 has_hashes = hashes and any (hash for hash in hashes .values ())
192243
193244 yielded = False
194-
195245 for line in self .write_header ():
196- yield line
246+ yield line , {}
197247 yielded = True
198248 for line in self .write_flags ():
199- yield line
249+ yield line , {}
200250 yielded = True
201251
202252 unsafe_requirements = unsafe_requirements or {
@@ -207,36 +257,36 @@ def _iter_lines(
207257 if packages :
208258 for ireq in sorted (packages , key = self ._sort_key ):
209259 if has_hashes and not hashes .get (ireq ):
210- yield MESSAGE_UNHASHED_PACKAGE
260+ yield MESSAGE_UNHASHED_PACKAGE , {}
211261 warn_uninstallable = True
212262 line = self ._format_requirement (
213263 ireq , markers .get (key_from_ireq (ireq )), hashes = hashes
214264 )
215- yield line
265+ yield line , self . _get_json ( ireq , line , hashes = hashes )
216266 yielded = True
217267
218268 if unsafe_requirements :
219- yield ""
269+ yield "" , {}
220270 yielded = True
221271 if has_hashes and not self .allow_unsafe :
222- yield MESSAGE_UNSAFE_PACKAGES_UNPINNED
272+ yield MESSAGE_UNSAFE_PACKAGES_UNPINNED , {}
223273 warn_uninstallable = True
224274 else :
225- yield MESSAGE_UNSAFE_PACKAGES
275+ yield MESSAGE_UNSAFE_PACKAGES , {}
226276
227277 for ireq in sorted (unsafe_requirements , key = self ._sort_key ):
228278 ireq_key = key_from_ireq (ireq )
229279 if not self .allow_unsafe :
230- yield comment (f"# { ireq_key } " )
280+ yield comment (f"# { ireq_key } " ), {}
231281 else :
232282 line = self ._format_requirement (
233283 ireq , marker = markers .get (ireq_key ), hashes = hashes
234284 )
235- yield line
285+ yield line , self . _get_json ( ireq , line , unsafe = True )
236286
237287 # Yield even when there's no real content, so that blank files are written
238288 if not yielded :
239- yield ""
289+ yield "" , {}
240290
241291 if warn_uninstallable :
242292 log .warning (MESSAGE_UNINSTALLABLE )
@@ -249,27 +299,33 @@ def write(
249299 markers : dict [str , Marker ],
250300 hashes : dict [InstallRequirement , set [str ]] | None ,
251301 ) -> None :
252- if not self .dry_run :
302+ output_structure = []
303+ if not self .dry_run or self .json_output :
253304 dst_file = io .TextIOWrapper (
254305 self .dst_file ,
255306 encoding = "utf8" ,
256307 newline = self .linesep ,
257308 line_buffering = True ,
258309 )
259310 try :
260- for line in self ._iter_lines (
311+ for line , ireq in self ._iter_ireqs (
261312 results , unsafe_requirements , unsafe_packages , markers , hashes
262313 ):
263314 if self .dry_run :
264315 # Bypass the log level to always print this during a dry run
265316 log .log (line )
266317 else :
267- log .info (line )
318+ if not self .json_output :
319+ log .info (line )
268320 dst_file .write (unstyle (line ))
269321 dst_file .write ("\n " )
322+ if self .json_output and ireq :
323+ output_structure .append (ireq )
270324 finally :
271- if not self .dry_run :
325+ if not self .dry_run or self . json_output :
272326 dst_file .detach ()
327+ if self .json_output :
328+ print (json .dumps (output_structure , indent = 4 ))
273329
274330 def _format_requirement (
275331 self ,
0 commit comments