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..2aa8bd3b846
--- /dev/null
+++ b/docs/html/cli/pip_inspect.rst
@@ -0,0 +1,33 @@
+.. _`pip inspect`:
+
+===========
+pip inspect
+===========
+
+.. versionadded:: 22.2
+
+
+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 749e88096aa..63ce1ca4dbf 100644
--- a/docs/html/reference/index.md
+++ b/docs/html/reference/index.md
@@ -10,4 +10,5 @@ build-system/index
requirement-specifiers
requirements-file-format
installation-report
+inspect-report
```
diff --git a/docs/html/reference/inspect-report.md b/docs/html/reference/inspect-report.md
new file mode 100644
index 00000000000..4d367da9de8
--- /dev/null
+++ b/docs/html/reference/inspect-report.md
@@ -0,0 +1,217 @@
+# `pip inspect` JSON output specification
+
+```{versionadded} 22.2
+```
+
+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](InspectReportItem) 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.
+
+(InspectReportItem)=
+
+An `InspectReportItem` is an object describing an 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 corresponds to 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
+
+Running the ``pip inspect`` command, in an environment where `pip` is installed in
+editable mode and `packaging` is installed as well, will produce an output similar to
+this (metadata abriged for brevity):
+
+```json
+{
+ "version": "0",
+ "pip_version": "22.2.dev0",
+ "installed": [
+ {
+ "metadata": {
+ "metadata_version": "2.1",
+ "name": "pyparsing",
+ "version": "3.0.9",
+ "summary": "pyparsing module - Classes and methods to define and execute parsing grammars",
+ "description_content_type": "text/x-rst",
+ "author_email": "Paul McGuire ",
+ "classifier": [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Information Technology",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ "Typing :: Typed"
+ ],
+ "requires_dist": [
+ "railroad-diagrams ; extra == \"diagrams\"",
+ "jinja2 ; extra == \"diagrams\""
+ ],
+ "requires_python": ">=3.6.8",
+ "project_url": [
+ "Homepage, https://github.com/pyparsing/pyparsing/"
+ ],
+ "provides_extra": [
+ "diagrams"
+ ],
+ "description": "..."
+ },
+ "metadata_location": "/home/me/.virtualenvs/demoenv/lib/python3.8/site-packages/pyparsing-3.0.9.dist-info",
+ "installer": "pip",
+ "requested": false
+ },
+ {
+ "metadata": {
+ "metadata_version": "2.1",
+ "name": "packaging",
+ "version": "21.3",
+ "platform": [
+ "UNKNOWN"
+ ],
+ "summary": "Core utilities for Python packages",
+ "description_content_type": "text/x-rst",
+ "home_page": "https://github.com/pypa/packaging",
+ "author": "Donald Stufft and individual contributors",
+ "author_email": "donald@stufft.io",
+ "license": "BSD-2-Clause or Apache-2.0",
+ "classifier": [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "License :: OSI Approved :: BSD License",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy"
+ ],
+ "requires_dist": [
+ "pyparsing (!=3.0.5,>=2.0.2)"
+ ],
+ "requires_python": ">=3.6",
+ "description": "..."
+ },
+ "metadata_location": "/home/me/.virtualenvs/demoenv/lib/python3.8/site-packages/packaging-21.3.dist-info",
+ "installer": "pip",
+ "requested": true
+ },
+ {
+ "metadata": {
+ "metadata_version": "2.1",
+ "name": "pip",
+ "version": "22.2.dev0",
+ "summary": "The PyPA recommended tool for installing Python packages.",
+ "home_page": "https://pip.pypa.io/",
+ "author": "The pip developers",
+ "author_email": "distutils-sig@python.org",
+ "license": "MIT",
+ "classifier": [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Topic :: Software Development :: Build Tools",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy"
+ ],
+ "requires_python": ">=3.7",
+ "project_url": [
+ "Documentation, https://pip.pypa.io",
+ "Source, https://github.com/pypa/pip",
+ "Changelog, https://pip.pypa.io/en/stable/news/"
+ ],
+ "description": "..."
+ },
+ "metadata_location": "/home/me/pip/src/pip.egg-info",
+ "direct_url": {
+ "url": "file:///home/me/pip/src",
+ "dir_info": {
+ "editable": true
+ }
+ }
+ }
+ ],
+ "environment": {
+ "implementation_name": "cpython",
+ "implementation_version": "3.8.10",
+ "os_name": "posix",
+ "platform_machine": "x86_64",
+ "platform_release": "5.13-generic",
+ "platform_system": "Linux",
+ "platform_version": "...",
+ "python_full_version": "3.8.10",
+ "platform_python_implementation": "CPython",
+ "python_version": "3.8",
+ "sys_platform": "linux"
+ }
+}
+```
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..a4e3599306e
--- /dev/null
+++ b/src/pip/_internal/commands/inspect.py
@@ -0,0 +1,97 @@
+import logging
+from optparse import Values
+from typing import Any, Dict, List
+
+from pip._vendor.packaging.markers import default_environment
+from pip._vendor.rich import print_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
+
+logger = logging.getLogger(__name__)
+
+
+class InspectCommand(Command):
+ """
+ Inspect the content of a Python environment and produce a report in JSON format.
+ """
+
+ ignore_require_venv = True
+ usage = """
+ %prog [options]"""
+
+ def add_options(self) -> None:
+ self.cmd_opts.add_option(
+ "--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:
+ logger.warning(
+ "pip inspect is currently an experimental command. "
+ "The output format may change in a future release without prior warning."
+ )
+
+ 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?
+ }
+ print_json(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
diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py
index 10a6c489783..151fd6d009e 100644
--- a/src/pip/_internal/metadata/base.py
+++ b/src/pip/_internal/metadata/base.py
@@ -311,6 +311,10 @@ def installer(self) -> str:
return cleaned_line
return ""
+ @property
+ def requested(self) -> bool:
+ return self.is_file("REQUESTED")
+
@property
def editable(self) -> bool:
return bool(self.editable_project_location)
diff --git a/tests/functional/test_inspect.py b/tests/functional/test_inspect.py
new file mode 100644
index 00000000000..464bdbaa11e
--- /dev/null
+++ b/tests/functional/test_inspect.py
@@ -0,0 +1,45 @@
+import json
+
+import pytest
+
+from tests.conftest import ScriptFactory
+from tests.lib import PipTestEnvironment, TestData
+
+
+@pytest.fixture(scope="session")
+def simple_script(
+ tmpdir_factory: pytest.TempPathFactory,
+ script_factory: ScriptFactory,
+ shared_data: TestData,
+) -> PipTestEnvironment:
+ tmpdir = tmpdir_factory.mktemp("pip_test_package")
+ script = script_factory(tmpdir.joinpath("workspace"))
+ script.pip(
+ "install",
+ "-f",
+ shared_data.find_links,
+ "--no-index",
+ "simplewheel==1.0",
+ )
+ return script
+
+
+def test_inspect_basic(simple_script: PipTestEnvironment) -> None:
+ """
+ Test default behavior of inspect command.
+ """
+ result = simple_script.pip("inspect", allow_stderr_warning=True)
+ report = json.loads(result.stdout)
+ installed = report["installed"]
+ assert len(installed) == 4
+ installed_by_name = {i["metadata"]["name"]: i for i in installed}
+ assert installed_by_name.keys() == {
+ "pip",
+ "setuptools",
+ "coverage",
+ "simplewheel",
+ }
+ assert installed_by_name["simplewheel"]["metadata"]["version"] == "1.0"
+ assert installed_by_name["simplewheel"]["requested"] is True
+ assert installed_by_name["simplewheel"]["installer"] == "pip"
+ assert "environment" in report