Skip to content

Commit

Permalink
Add pip inspect command
Browse files Browse the repository at this point in the history
  • Loading branch information
sbidoul committed Jul 12, 2022
1 parent 20ed685 commit 29e7a09
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/html/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pip
pip_install
pip_uninstall
pip_inspect
pip_list
pip_show
pip_freeze
Expand Down
32 changes: 32 additions & 0 deletions docs/html/cli/pip_inspect.rst
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions docs/html/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ interoperability standards that pip utilises/implements.
build-system/index
requirement-specifiers
requirements-file-format
inspect-report
```
79 changes: 79 additions & 0 deletions docs/html/reference/inspect-report.md
Original file line number Diff line number Diff line change
@@ -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": {}
}
```
2 changes: 2 additions & 0 deletions news/11245.feature.rst
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions src/pip/_internal/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
91 changes: 91 additions & 0 deletions src/pip/_internal/commands/inspect.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 29e7a09

Please sign in to comment.