diff --git a/docs/html/cli/index.md b/docs/html/cli/index.md index a3497c308c2..4b56dbeb4cb 100644 --- a/docs/html/cli/index.md +++ b/docs/html/cli/index.md @@ -16,6 +16,7 @@ pip pip_install pip_uninstall +pip_inspect pip_list pip_show pip_freeze diff --git a/docs/html/cli/pip_inspect.rst b/docs/html/cli/pip_inspect.rst new file mode 100644 index 00000000000..66217735ed1 --- /dev/null +++ b/docs/html/cli/pip_inspect.rst @@ -0,0 +1,32 @@ +.. _`pip inspect`: + +=========== +pip inspect +=========== + + + +Usage +===== + +.. tab:: Unix/macOS + + .. pip-command-usage:: inspect "python -m pip" + +.. tab:: Windows + + .. pip-command-usage:: inspect "py -m pip" + + +Description +=========== + +.. pip-command-description:: inspect + +The format of the JSON output is described in :doc:`../reference/inspect-report`. + + +Options +======= + +.. pip-command-options:: inspect diff --git a/docs/html/reference/index.md b/docs/html/reference/index.md index 855dc79b37a..8b199a4f2d2 100644 --- a/docs/html/reference/index.md +++ b/docs/html/reference/index.md @@ -9,4 +9,5 @@ interoperability standards that pip utilises/implements. build-system/index requirement-specifiers requirements-file-format +inspect-report ``` diff --git a/docs/html/reference/inspect-report.md b/docs/html/reference/inspect-report.md new file mode 100644 index 00000000000..e482741da89 --- /dev/null +++ b/docs/html/reference/inspect-report.md @@ -0,0 +1,79 @@ +# `pip inspect` JSON output specification + +The `pip inspect` command produces a detailed JSON report of the Python +environment, including installed distributions. + +## Specification + +The report is a JSON object with the following properties: + +- `version`: the string `0`, denoting that the inspect command is an experimental + feature. This value will change to `1`, when the feature is deemed stable after + gathering user feedback (likely in pip 22.3 or 23.0). Backward incompatible changes + may be introduced in version `1` without notice. After that, it will change only if + and when backward incompatible changes are introduced, such as removing mandatory + fields or changing the semantics or data type of existing fields. The introduction of + backward incompatible changes will follow the usual pip processes such as the + deprecation cycle or feature flags. Tools must check this field to ensure they support + the corresponding version. + +- `pip_version`: a string with the version of pip used to produce the report. + +- `installed`: an array of `InspectReportItem` (see below) representing the + distribution packages that are installed. + +- `environment`: an object describing the environment where the installation report was + generated. See [PEP 508 environment + markers](https://peps.python.org/pep-0508/#environment-markers) for more information. + Values have a string type. + +An `InspectReportItem` is an object describing a (to be) installed distribution +package with the following properties: + +- `metadata`: the metadata of the distribution, converted to a JSON object according to + the [PEP 566 + transformation](https://www.python.org/dev/peps/pep-0566/#json-compatible-metadata). + +- `metadata_location`: the location of the metadata of the installed distribution. Most + of the time this is the `.dist-info` directory. For legacy installs it is the + `.egg-info` directory. + + ```{warning} + This field may not necessary point to a directory, for instance, in the case of older + `.egg` installs. + ``` + +- `direct_url`: Information about the direct URL that was used for installation, if any, + using the [direct + URL](https://packaging.python.org/en/latest/specifications/direct-url/) data + structure. In most case, this field the `direct_url.json` metadata, except for legacy + editable installs, where it is emulated. + +- `requested`: `true` if the `REQUESTED` metadata is present, `false` otherwise. This + field is only present for modern `.dist-info` installations. + + ```{note} + The `REQUESTED` metadata may not be generated by all installers. + It is generated by pip since version 20.2. + ``` + +- `installer`: the content of the `INSTALLER` metadata, if present and not empty. + +## Example + +The following command: + +```console +pip inspect +``` + +will produce an output similar to this (metadata abriged for brevity): + +```json +{ + "version": "0", + "pip_version": "22.2", + "installed": [], + "environment": {} +} +``` diff --git a/news/11245.feature.rst b/news/11245.feature.rst new file mode 100644 index 00000000000..ba80a8976da --- /dev/null +++ b/news/11245.feature.rst @@ -0,0 +1,2 @@ +Add ``pip inspect`` command to obtain the list of installed distributions and other +information about the Python environment, in JSON format. diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index c72f24f30e2..858a4101416 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -38,6 +38,11 @@ "FreezeCommand", "Output installed packages in requirements format.", ), + "inspect": CommandInfo( + "pip._internal.commands.inspect", + "InspectCommand", + "Inspect the python environment.", + ), "list": CommandInfo( "pip._internal.commands.list", "ListCommand", diff --git a/src/pip/_internal/commands/inspect.py b/src/pip/_internal/commands/inspect.py new file mode 100644 index 00000000000..975b2b5db33 --- /dev/null +++ b/src/pip/_internal/commands/inspect.py @@ -0,0 +1,91 @@ +from optparse import Values +from typing import Any, Dict, List + +from pip._vendor import rich +from pip._vendor.packaging.markers import default_environment +from pip._vendor.rich.json import JSON + +from pip import __version__ +from pip._internal.cli import cmdoptions +from pip._internal.cli.req_command import Command +from pip._internal.cli.status_codes import SUCCESS +from pip._internal.metadata import BaseDistribution, get_environment +from pip._internal.utils.compat import stdlib_pkgs +from pip._internal.utils.urls import path_to_url + + +class InspectCommand(Command): + """ + Inspect the content of a Python environment. + """ + + ignore_require_venv = True + usage = """ + %prog [options]""" + + def add_options(self) -> None: + self.cmd_opts.add_option( + "-l", + "--local", + action="store_true", + default=False, + help=( + "If in a virtualenv that has global access, do not list " + "globally-installed packages." + ), + ) + self.cmd_opts.add_option( + "--user", + dest="user", + action="store_true", + default=False, + help="Only output packages installed in user-site.", + ) + self.cmd_opts.add_option(cmdoptions.list_path()) + self.parser.insert_option_group(0, self.cmd_opts) + + def run(self, options: Values, args: List[str]) -> int: + cmdoptions.check_list_path_option(options) + dists = get_environment(options.path).iter_installed_distributions( + local_only=options.local, + user_only=options.user, + skip=set(stdlib_pkgs), + ) + output = { + "version": "0", + "pip_version": __version__, + "installed": [self._dist_to_dict(dist) for dist in dists], + "environment": default_environment(), + # TODO tags? scheme? + } + rich.print(JSON.from_data(output)) + return SUCCESS + + def _dist_to_dict(self, dist: BaseDistribution) -> Dict[str, Any]: + res: Dict[str, Any] = { + "metadata": dist.metadata_dict, + "metadata_location": dist.info_location, + } + # direct_url. Note that we don't have download_info (as in the installation + # report) since it is not recorded in installed metadata. + direct_url = dist.direct_url + if direct_url is not None: + res["direct_url"] = direct_url.to_dict() + else: + # Emulate direct_url for legacy editable installs. + editable_project_location = dist.editable_project_location + if editable_project_location is not None: + res["direct_url"] = { + "url": path_to_url(editable_project_location), + "dir_info": { + "editable": True, + }, + } + # installer + installer = dist.installer + if dist.installer: + res["installer"] = installer + # requested + if dist.installed_with_dist_info: + res["requested"] = dist.requested + return res