diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 55edb280c96..1cc7f662244 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -1,4 +1,5 @@ import errno +import json import operator import os import shutil @@ -21,6 +22,7 @@ from pip._internal.locations import get_scheme from pip._internal.metadata import get_environment from pip._internal.models.format_control import FormatControl +from pip._internal.models.installation_report import InstallationReport from pip._internal.operations.build.build_tracker import get_build_tracker from pip._internal.operations.check import ConflictDetails, check_install_conflicts from pip._internal.req import install_given_reqs @@ -223,6 +225,20 @@ def add_options(self) -> None: default=True, help="Do not warn about broken dependencies", ) + + self.cmd_opts.add_option( + "--report", + dest="json_report_file", + metavar="file", + default=None, + help=( + "Generate a JSON file describing what pip did to install " + "the provided requirements. " + "Can be used in combination with --dry-run and --ignore-installed " + "to 'resolve' the requirements." + ), + ) + self.cmd_opts.add_option(cmdoptions.no_binary()) self.cmd_opts.add_option(cmdoptions.only_binary()) self.cmd_opts.add_option(cmdoptions.prefer_binary()) @@ -340,6 +356,10 @@ def run(self, options: Values, args: List[str]) -> int: requirement_set = resolver.resolve( reqs, check_supported_wheels=not options.target_dir ) + if options.json_report_file: + report = InstallationReport.from_requirement_set(requirement_set) + with open(options.json_report_file, "w") as f: + json.dump(report.to_json(), f) try: pip_req = requirement_set.get_requirement("pip") diff --git a/src/pip/_internal/models/installation_report.py b/src/pip/_internal/models/installation_report.py new file mode 100644 index 00000000000..be59d85fd14 --- /dev/null +++ b/src/pip/_internal/models/installation_report.py @@ -0,0 +1,68 @@ +from typing import Any, Dict + +from pip._internal.req.req_install import InstallRequirement +from pip._internal.req.req_set import RequirementSet +from pip._internal.utils.direct_url_helpers import direct_url_for_editable + + +class InstallationReportItem: + def __init__(self, install_req: InstallRequirement): + self._install_req = install_req + + def to_json(self) -> Dict[str, Any]: + is_direct = bool(self._install_req.original_link) + assert ( + self._install_req.download_info + ), f"No download_info for {self._install_req}" + if self._install_req.editable: + download_info = direct_url_for_editable( + self._install_req.unpacked_source_directory + ) + else: + download_info = self._install_req.download_info + res = { + # is_direct is true if requirement came from a direct URL reference (which + # includes editable requirements), and false if the requirement was + # downloaded from a PEP 503 index or --find-links. + "is_direct": is_direct, + # PEP 610 json for the download URL + "download_info": download_info.to_dict(), + # PEP 566 json encoding for metadata + # https://www.python.org/dev/peps/pep-0566/#json-compatible-metadata + # TODO (MVP?) self._install_req.metadata.to_json() + "metadata": { + "name": self._install_req.name, + "version": str(self._install_req.get_dist().version), + }, + } + if self._install_req.user_supplied: + # TODO (MVP) investigate why self._install_req.req does not reproduce the + # user supplied URL in case of direct requirements + if self._install_req.original_link: + res["requested"] = str(self._install_req.original_link) + else: + res["requested"] = str(self._install_req.req) + # TODO (LATER) information about the index or find-links for non-direct reqs + # TODO (LATER) information about pip install options + return res + + +class InstallationReport: + def __init__(self, items: Dict[str, InstallationReportItem]): + self._items = items + + @classmethod + def from_requirement_set( + cls, requirement_set: RequirementSet + ) -> "InstallationReport": + items = {} + for name, requirement in requirement_set.requirements.items(): + item = InstallationReportItem(requirement) + items[name] = item + return InstallationReport(items) + + def to_json(self) -> Dict[str, Any]: + # TODO (MVP?) platform information (python version, etc) + return { + "installed": {name: item.to_json() for name, item in self._items.items()} + }