Skip to content

Commit 0dc358d

Browse files
committed
Write JSON output to requirements.json
1 parent b524ee2 commit 0dc358d

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
@@ -41,6 +41,7 @@
4141
)
4242
DEFAULT_REQUIREMENTS_FILE = "requirements.in"
4343
DEFAULT_REQUIREMENTS_OUTPUT_FILE = "requirements.txt"
44+
DEFAULT_REQUIREMENTS_OUTPUT_FILE_JSON = "requirements.json"
4445
METADATA_FILENAMES = frozenset({"setup.py", "setup.cfg", "pyproject.toml"})
4546

4647

@@ -220,10 +221,16 @@ def cli(
220221
# An output file must be provided for stdin
221222
if src_files == ("-",):
222223
raise click.BadParameter("--output-file is required if input is from stdin")
223-
# Use default requirements output file if there is a setup.py the source file
224+
# Use default requirements output file if the source file is a recognized
225+
# packaging metadata file
224226
elif os.path.basename(src_files[0]) in METADATA_FILENAMES:
225227
file_name = os.path.join(
226-
os.path.dirname(src_files[0]), DEFAULT_REQUIREMENTS_OUTPUT_FILE
228+
os.path.dirname(src_files[0]),
229+
(
230+
DEFAULT_REQUIREMENTS_OUTPUT_FILE_JSON
231+
if json
232+
else DEFAULT_REQUIREMENTS_OUTPUT_FILE
233+
),
227234
)
228235
# An output file must be provided if there are multiple source files
229236
elif len(src_files) > 1:
@@ -302,7 +309,7 @@ def cli(
302309
# Proxy with a LocalRequirementsRepository if --upgrade is not specified
303310
# (= default invocation)
304311
output_file_exists = os.path.exists(output_file.name)
305-
if not upgrade and output_file_exists:
312+
if not (upgrade or json) and output_file_exists:
306313
output_file_is_empty = os.path.getsize(output_file.name) == 0
307314
if upgrade_install_reqs and output_file_is_empty:
308315
log.warning(

piptools/writer.py

Lines changed: 79 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ def _iter_ireqs(
201201
unsafe_packages: set[str],
202202
markers: dict[str, Marker],
203203
hashes: dict[InstallRequirement, set[str]] | None = None,
204-
) -> Iterator[str, dict[str, str]]:
204+
) -> Iterator[str] | Iterator[dict[str, str | list[str]]]:
205205
# default values
206206
unsafe_packages = unsafe_packages if self.allow_unsafe else set()
207207
hashes = hashes or {}
@@ -212,12 +212,13 @@ def _iter_ireqs(
212212
has_hashes = hashes and any(hash for hash in hashes.values())
213213

214214
yielded = False
215-
for line in self.write_header():
216-
yield line, {}
217-
yielded = True
218-
for line in self.write_flags():
219-
yield line, {}
220-
yielded = True
215+
if not self.json_output:
216+
for line in self.write_header():
217+
yield line
218+
yielded = True
219+
for line in self.write_flags():
220+
yield line
221+
yielded = True
221222

222223
unsafe_requirements = unsafe_requirements or {
223224
r for r in results if r.name in unsafe_packages
@@ -226,37 +227,39 @@ def _iter_ireqs(
226227

227228
if packages:
228229
for ireq in sorted(packages, key=self._sort_key):
229-
if has_hashes and not hashes.get(ireq):
230-
yield MESSAGE_UNHASHED_PACKAGE, {}
230+
if has_hashes and not hashes.get(ireq) and not self.json_output:
231+
yield MESSAGE_UNHASHED_PACKAGE
231232
warn_uninstallable = True
232-
line, json = self._format_requirement(
233+
formatted_req = self._format_requirement(
233234
ireq, markers.get(key_from_ireq(ireq)), hashes=hashes
234235
)
235-
yield line, json
236+
yield formatted_req
236237
yielded = True
237238

238239
if unsafe_requirements:
239-
yield "", {}
240+
241+
if not self.json_output:
242+
yield ""
240243
yielded = True
241-
if has_hashes and not self.allow_unsafe:
242-
yield MESSAGE_UNSAFE_PACKAGES_UNPINNED, {}
244+
if has_hashes and not self.allow_unsafe and not self.json_output:
245+
yield MESSAGE_UNSAFE_PACKAGES_UNPINNED
243246
warn_uninstallable = True
244-
else:
245-
yield MESSAGE_UNSAFE_PACKAGES, {}
247+
elif not self.json_output:
248+
yield MESSAGE_UNSAFE_PACKAGES
246249

247250
for ireq in sorted(unsafe_requirements, key=self._sort_key):
248251
ireq_key = key_from_ireq(ireq)
249-
if not self.allow_unsafe:
250-
yield comment(f"# {ireq_key}"), {}
252+
if not self.allow_unsafe and not self.json_output:
253+
yield comment(f"# {ireq_key}")
251254
else:
252-
line, json = self._format_requirement(
255+
formatted_req = self._format_requirement(
253256
ireq, marker=markers.get(ireq_key), hashes=hashes
254257
)
255-
yield line, json
258+
yield formatted_req
256259

257260
# Yield even when there's no real content, so that blank files are written
258261
if not yielded:
259-
yield "", {}
262+
yield ""
260263

261264
if warn_uninstallable:
262265
log.warning(MESSAGE_UNINSTALLABLE)
@@ -270,40 +273,47 @@ def write(
270273
hashes: dict[InstallRequirement, set[str]] | None,
271274
) -> None:
272275
output_structure = []
273-
if not self.dry_run or self.json_output:
276+
if not self.dry_run:
274277
dst_file = io.TextIOWrapper(
275278
self.dst_file,
276279
encoding="utf8",
277280
newline=self.linesep,
278281
line_buffering=True,
279282
)
280283
try:
281-
for line, ireq in self._iter_ireqs(
284+
for formatted_req in self._iter_ireqs(
282285
results, unsafe_requirements, unsafe_packages, markers, hashes
283286
):
284-
if self.dry_run:
287+
if self.dry_run and not self.json_output:
285288
# Bypass the log level to always print this during a dry run
286-
log.log(line)
289+
assert isinstance(formatted_req, str)
290+
log.log(formatted_req)
287291
else:
288292
if not self.json_output:
289-
log.info(line)
290-
dst_file.write(unstyle(line))
291-
dst_file.write("\n")
292-
if self.json_output and ireq:
293-
output_structure.append(ireq)
293+
assert isinstance(formatted_req, str)
294+
log.info(formatted_req)
295+
dst_file.write(unstyle(formatted_req))
296+
dst_file.write("\n")
297+
else:
298+
output_structure.append(formatted_req)
294299
finally:
295-
if not self.dry_run or self.json_output:
296-
dst_file.detach()
297300
if self.json_output:
301+
json.dump(output_structure, dst_file, indent=4)
298302
print(json.dumps(output_structure, indent=4))
303+
if not self.dry_run:
304+
dst_file.detach()
299305

300306
def _format_requirement(
301307
self,
302308
ireq: InstallRequirement,
303309
marker: Marker | None = None,
304310
hashes: dict[InstallRequirement, set[str]] | None = None,
305311
unsafe: bool = False,
306-
) -> tuple[str, dict[str, str | list[str]]]:
312+
) -> str | dict[str, str | list[str]]:
313+
"""Format a given ``InstallRequirement``.
314+
315+
:returns: A line or a JSON structure to be written to the output file.
316+
"""
307317
ireq_hashes = (hashes if hashes is not None else {}).get(ireq)
308318

309319
line = format_requirement(ireq, marker=marker, hashes=ireq_hashes)
@@ -344,36 +354,40 @@ def _format_requirement(
344354
if self.annotate:
345355
line = "\n".join(ln.rstrip() for ln in lines)
346356

347-
hashable = True
348-
if ireq.link:
349-
if ireq.link.is_vcs or (ireq.link.is_file and ireq.link.is_existing_dir()):
350-
hashable = False
351-
output_marker = ""
352-
if marker:
353-
output_marker = str(marker)
354-
via = []
355-
for parent_req in required_by:
356-
if parent_req.startswith("-r "):
357-
# Ensure paths to requirements files given are absolute
358-
reqs_in_path = os.path.abspath(parent_req[len("-r ") :])
359-
via.append(f"-r {reqs_in_path}")
360-
else:
361-
via.append(parent_req)
362-
output_hashes = []
363-
if ireq_hashes:
364-
output_hashes = list(ireq_hashes)
365-
366-
ireq_json = {
367-
"name": ireq.name,
368-
"version": str(ireq.specifier).lstrip("=="),
369-
"requirement": str(ireq.req),
370-
"via": via,
371-
"line": unstyle(line),
372-
"hashable": hashable,
373-
"editable": ireq.editable,
374-
"hashes": output_hashes,
375-
"marker": output_marker,
376-
"unsafe": unsafe,
377-
}
357+
if self.json_output:
358+
hashable = True
359+
if ireq.link:
360+
if ireq.link.is_vcs or (
361+
ireq.link.is_file and ireq.link.is_existing_dir()
362+
):
363+
hashable = False
364+
output_marker = ""
365+
if marker:
366+
output_marker = str(marker)
367+
via = []
368+
for parent_req in required_by:
369+
if parent_req.startswith("-r "):
370+
# Ensure paths to requirements files given are absolute
371+
reqs_in_path = os.path.abspath(parent_req[len("-r ") :])
372+
via.append(f"-r {reqs_in_path}")
373+
else:
374+
via.append(parent_req)
375+
output_hashes = []
376+
if ireq_hashes:
377+
output_hashes = list(ireq_hashes)
378+
379+
ireq_json = {
380+
"name": ireq.name,
381+
"version": str(ireq.specifier).lstrip("=="),
382+
"requirement": str(ireq.req),
383+
"via": via,
384+
"line": unstyle(line),
385+
"hashable": hashable,
386+
"editable": ireq.editable,
387+
"hashes": output_hashes,
388+
"marker": output_marker,
389+
"unsafe": unsafe,
390+
}
391+
return ireq_json
378392

379-
return line, ireq_json
393+
return line

0 commit comments

Comments
 (0)