Skip to content

Commit 2fc69aa

Browse files
committed
DRAFT - installation report
1 parent 828da47 commit 2fc69aa

File tree

2 files changed

+86
-0
lines changed

2 files changed

+86
-0
lines changed

src/pip/_internal/commands/install.py

+20
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import errno
2+
import json
23
import operator
34
import os
45
import shutil
@@ -21,6 +22,7 @@
2122
from pip._internal.locations import get_scheme
2223
from pip._internal.metadata import get_environment
2324
from pip._internal.models.format_control import FormatControl
25+
from pip._internal.models.installation_report import InstallationReport
2426
from pip._internal.operations.build.build_tracker import get_build_tracker
2527
from pip._internal.operations.check import ConflictDetails, check_install_conflicts
2628
from pip._internal.req import install_given_reqs
@@ -223,6 +225,20 @@ def add_options(self) -> None:
223225
default=True,
224226
help="Do not warn about broken dependencies",
225227
)
228+
229+
self.cmd_opts.add_option(
230+
"--report",
231+
dest="json_report_file",
232+
metavar="file",
233+
default=None,
234+
help=(
235+
"Generate a JSON file describing what pip did to install "
236+
"the provided requirements. "
237+
"Can be used in combination with --dry-run and --ignore-installed "
238+
"to 'resolve' the requirements."
239+
),
240+
)
241+
226242
self.cmd_opts.add_option(cmdoptions.no_binary())
227243
self.cmd_opts.add_option(cmdoptions.only_binary())
228244
self.cmd_opts.add_option(cmdoptions.prefer_binary())
@@ -340,6 +356,10 @@ def run(self, options: Values, args: List[str]) -> int:
340356
requirement_set = resolver.resolve(
341357
reqs, check_supported_wheels=not options.target_dir
342358
)
359+
if options.json_report_file:
360+
report = InstallationReport.from_requirement_set(requirement_set)
361+
with open(options.json_report_file, "w") as f:
362+
json.dump(report.to_json(), f)
343363

344364
try:
345365
pip_req = requirement_set.get_requirement("pip")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from typing import Any, Dict
2+
3+
from pip._internal.req.req_install import InstallRequirement
4+
from pip._internal.req.req_set import RequirementSet
5+
from pip._internal.utils.direct_url_helpers import direct_url_for_editable
6+
7+
8+
class InstallationReportItem:
9+
def __init__(self, install_req: InstallRequirement):
10+
self._install_req = install_req
11+
12+
def to_json(self) -> Dict[str, Any]:
13+
is_direct = bool(self._install_req.original_link)
14+
assert (
15+
self._install_req.download_info
16+
), f"No download_info for {self._install_req}"
17+
if self._install_req.editable:
18+
download_info = direct_url_for_editable(
19+
self._install_req.unpacked_source_directory
20+
)
21+
else:
22+
download_info = self._install_req.download_info
23+
res = {
24+
# is_direct is true if requirement came from a direct URL reference (which
25+
# includes editable requirements), and false if the requirement was
26+
# downloaded from a PEP 503 index or --find-links.
27+
"is_direct": is_direct,
28+
# PEP 610 json for the download URL
29+
"download_info": download_info.to_dict(),
30+
# PEP 566 json encoding for metadata
31+
# https://www.python.org/dev/peps/pep-0566/#json-compatible-metadata
32+
# TODO (MVP?) self._install_req.metadata.to_json()
33+
"metadata": {
34+
"name": self._install_req.name,
35+
"version": str(self._install_req.get_dist().version),
36+
},
37+
}
38+
if self._install_req.user_supplied:
39+
# TODO (MVP) investigate why self._install_req.req does not reproduce the
40+
# user supplied URL in case of direct requirements
41+
if self._install_req.original_link:
42+
res["requested"] = str(self._install_req.original_link)
43+
else:
44+
res["requested"] = str(self._install_req.req)
45+
# TODO (LATER) information about the index or find-links for non-direct reqs
46+
# TODO (LATER) information about pip install options
47+
return res
48+
49+
50+
class InstallationReport:
51+
def __init__(self, items: Dict[str, InstallationReportItem]):
52+
self._items = items
53+
54+
@classmethod
55+
def from_requirement_set(
56+
cls, requirement_set: RequirementSet
57+
) -> "InstallationReport":
58+
items = {}
59+
for name, requirement in requirement_set.requirements.items():
60+
item = InstallationReportItem(requirement)
61+
items[name] = item
62+
return InstallationReport(items)
63+
64+
def to_json(self) -> Dict[str, Any]:
65+
# TODO (LATER) platform information (python version, etc)
66+
return {"install": {name: item.to_json() for name, item in self._items.items()}}

0 commit comments

Comments
 (0)