Skip to content

Commit 69c0cb9

Browse files
committed
Write JSON output to requirements.json
1 parent 2f0b266 commit 69c0cb9

File tree

2 files changed

+89
-68
lines changed

2 files changed

+89
-68
lines changed

piptools/scripts/compile.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
)
3737
DEFAULT_REQUIREMENTS_FILE = "requirements.in"
3838
DEFAULT_REQUIREMENTS_OUTPUT_FILE = "requirements.txt"
39+
DEFAULT_REQUIREMENTS_OUTPUT_FILE_JSON = "requirements.json"
3940
METADATA_FILENAMES = frozenset({"setup.py", "setup.cfg", "pyproject.toml"})
4041

4142

@@ -215,10 +216,16 @@ def cli(
215216
# An output file must be provided for stdin
216217
if src_files == ("-",):
217218
raise click.BadParameter("--output-file is required if input is from stdin")
218-
# Use default requirements output file if there is a setup.py the source file
219+
# Use default requirements output file if the source file is a recognized
220+
# packaging metadata file
219221
elif os.path.basename(src_files[0]) in METADATA_FILENAMES:
220222
file_name = os.path.join(
221-
os.path.dirname(src_files[0]), DEFAULT_REQUIREMENTS_OUTPUT_FILE
223+
os.path.dirname(src_files[0]),
224+
(
225+
DEFAULT_REQUIREMENTS_OUTPUT_FILE_JSON
226+
if json
227+
else DEFAULT_REQUIREMENTS_OUTPUT_FILE
228+
),
222229
)
223230
# An output file must be provided if there are multiple source files
224231
elif len(src_files) > 1:
@@ -297,7 +304,7 @@ def cli(
297304
# Proxy with a LocalRequirementsRepository if --upgrade is not specified
298305
# (= default invocation)
299306
output_file_exists = os.path.exists(output_file.name)
300-
if not upgrade and output_file_exists:
307+
if not (upgrade or json) and output_file_exists:
301308
output_file_is_empty = os.path.getsize(output_file.name) == 0
302309
if upgrade_install_reqs and output_file_is_empty:
303310
log.warning(

piptools/writer.py

Lines changed: 79 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ def _iter_ireqs(
183183
unsafe_packages: set[str],
184184
markers: dict[str, Marker],
185185
hashes: dict[InstallRequirement, set[str]] | None = None,
186-
) -> Iterator[str, dict[str, str]]:
186+
) -> Iterator[str] | Iterator[dict[str, str | list[str]]]:
187187
# default values
188188
unsafe_packages = unsafe_packages if self.allow_unsafe else set()
189189
hashes = hashes or {}
@@ -194,12 +194,13 @@ def _iter_ireqs(
194194
has_hashes = hashes and any(hash for hash in hashes.values())
195195

196196
yielded = False
197-
for line in self.write_header():
198-
yield line, {}
199-
yielded = True
200-
for line in self.write_flags():
201-
yield line, {}
202-
yielded = True
197+
if not self.json_output:
198+
for line in self.write_header():
199+
yield line
200+
yielded = True
201+
for line in self.write_flags():
202+
yield line
203+
yielded = True
203204

204205
unsafe_requirements = unsafe_requirements or {
205206
r for r in results if r.name in unsafe_packages
@@ -208,37 +209,39 @@ def _iter_ireqs(
208209

209210
if packages:
210211
for ireq in sorted(packages, key=self._sort_key):
211-
if has_hashes and not hashes.get(ireq):
212-
yield MESSAGE_UNHASHED_PACKAGE, {}
212+
if has_hashes and not hashes.get(ireq) and not self.json_output:
213+
yield MESSAGE_UNHASHED_PACKAGE
213214
warn_uninstallable = True
214-
line, json = self._format_requirement(
215+
formatted_req = self._format_requirement(
215216
ireq, markers.get(key_from_ireq(ireq)), hashes=hashes
216217
)
217-
yield line, json
218+
yield formatted_req
218219
yielded = True
219220

220221
if unsafe_requirements:
221-
yield "", {}
222+
223+
if not self.json_output:
224+
yield ""
222225
yielded = True
223-
if has_hashes and not self.allow_unsafe:
224-
yield MESSAGE_UNSAFE_PACKAGES_UNPINNED, {}
226+
if has_hashes and not self.allow_unsafe and not self.json_output:
227+
yield MESSAGE_UNSAFE_PACKAGES_UNPINNED
225228
warn_uninstallable = True
226-
else:
227-
yield MESSAGE_UNSAFE_PACKAGES, {}
229+
elif not self.json_output:
230+
yield MESSAGE_UNSAFE_PACKAGES
228231

229232
for ireq in sorted(unsafe_requirements, key=self._sort_key):
230233
ireq_key = key_from_ireq(ireq)
231-
if not self.allow_unsafe:
232-
yield comment(f"# {ireq_key}"), {}
234+
if not self.allow_unsafe and not self.json_output:
235+
yield comment(f"# {ireq_key}")
233236
else:
234-
line, json = self._format_requirement(
237+
formatted_req = self._format_requirement(
235238
ireq, marker=markers.get(ireq_key), hashes=hashes
236239
)
237-
yield line, json
240+
yield formatted_req
238241

239242
# Yield even when there's no real content, so that blank files are written
240243
if not yielded:
241-
yield "", {}
244+
yield ""
242245

243246
if warn_uninstallable:
244247
log.warning(MESSAGE_UNINSTALLABLE)
@@ -252,40 +255,47 @@ def write(
252255
hashes: dict[InstallRequirement, set[str]] | None,
253256
) -> None:
254257
output_structure = []
255-
if not self.dry_run or self.json_output:
258+
if not self.dry_run:
256259
dst_file = io.TextIOWrapper(
257260
self.dst_file,
258261
encoding="utf8",
259262
newline=self.linesep,
260263
line_buffering=True,
261264
)
262265
try:
263-
for line, ireq in self._iter_ireqs(
266+
for formatted_req in self._iter_ireqs(
264267
results, unsafe_requirements, unsafe_packages, markers, hashes
265268
):
266-
if self.dry_run:
269+
if self.dry_run and not self.json_output:
267270
# Bypass the log level to always print this during a dry run
268-
log.log(line)
271+
assert isinstance(formatted_req, str)
272+
log.log(formatted_req)
269273
else:
270274
if not self.json_output:
271-
log.info(line)
272-
dst_file.write(unstyle(line))
273-
dst_file.write("\n")
274-
if self.json_output and ireq:
275-
output_structure.append(ireq)
275+
assert isinstance(formatted_req, str)
276+
log.info(formatted_req)
277+
dst_file.write(unstyle(formatted_req))
278+
dst_file.write("\n")
279+
else:
280+
output_structure.append(formatted_req)
276281
finally:
277-
if not self.dry_run or self.json_output:
278-
dst_file.detach()
279282
if self.json_output:
283+
json.dump(output_structure, dst_file, indent=4)
280284
print(json.dumps(output_structure, indent=4))
285+
if not self.dry_run:
286+
dst_file.detach()
281287

282288
def _format_requirement(
283289
self,
284290
ireq: InstallRequirement,
285291
marker: Marker | None = None,
286292
hashes: dict[InstallRequirement, set[str]] | None = None,
287293
unsafe: bool = False,
288-
) -> tuple[str, dict[str, str | list[str]]]:
294+
) -> str | dict[str, str | list[str]]:
295+
"""Format a given ``InstallRequirement``.
296+
297+
:returns: A line or a JSON structure to be written to the output file.
298+
"""
289299
ireq_hashes = (hashes if hashes is not None else {}).get(ireq)
290300

291301
line = format_requirement(ireq, marker=marker, hashes=ireq_hashes)
@@ -326,36 +336,40 @@ def _format_requirement(
326336
if self.annotate:
327337
line = "\n".join(ln.rstrip() for ln in lines)
328338

329-
hashable = True
330-
if ireq.link:
331-
if ireq.link.is_vcs or (ireq.link.is_file and ireq.link.is_existing_dir()):
332-
hashable = False
333-
output_marker = ""
334-
if marker:
335-
output_marker = str(marker)
336-
via = []
337-
for parent_req in required_by:
338-
if parent_req.startswith("-r "):
339-
# Ensure paths to requirements files given are absolute
340-
reqs_in_path = os.path.abspath(parent_req[len("-r ") :])
341-
via.append(f"-r {reqs_in_path}")
342-
else:
343-
via.append(parent_req)
344-
output_hashes = []
345-
if ireq_hashes:
346-
output_hashes = list(ireq_hashes)
347-
348-
ireq_json = {
349-
"name": ireq.name,
350-
"version": str(ireq.specifier).lstrip("=="),
351-
"requirement": str(ireq.req),
352-
"via": via,
353-
"line": unstyle(line),
354-
"hashable": hashable,
355-
"editable": ireq.editable,
356-
"hashes": output_hashes,
357-
"marker": output_marker,
358-
"unsafe": unsafe,
359-
}
339+
if self.json_output:
340+
hashable = True
341+
if ireq.link:
342+
if ireq.link.is_vcs or (
343+
ireq.link.is_file and ireq.link.is_existing_dir()
344+
):
345+
hashable = False
346+
output_marker = ""
347+
if marker:
348+
output_marker = str(marker)
349+
via = []
350+
for parent_req in required_by:
351+
if parent_req.startswith("-r "):
352+
# Ensure paths to requirements files given are absolute
353+
reqs_in_path = os.path.abspath(parent_req[len("-r ") :])
354+
via.append(f"-r {reqs_in_path}")
355+
else:
356+
via.append(parent_req)
357+
output_hashes = []
358+
if ireq_hashes:
359+
output_hashes = list(ireq_hashes)
360+
361+
ireq_json = {
362+
"name": ireq.name,
363+
"version": str(ireq.specifier).lstrip("=="),
364+
"requirement": str(ireq.req),
365+
"via": via,
366+
"line": unstyle(line),
367+
"hashable": hashable,
368+
"editable": ireq.editable,
369+
"hashes": output_hashes,
370+
"marker": output_marker,
371+
"unsafe": unsafe,
372+
}
373+
return ireq_json
360374

361-
return line, ireq_json
375+
return line

0 commit comments

Comments
 (0)