diff --git a/docs/html/cli/pip_list.rst b/docs/html/cli/pip_list.rst
index 739435981c3..7d35dcf5569 100644
--- a/docs/html/cli/pip_list.rst
+++ b/docs/html/cli/pip_list.rst
@@ -104,19 +104,31 @@ Examples
#. Use json formatting
+ The ``metadata`` field is the distribution metadata, converted to JSON using the
+ transformation described in `PEP 566
+ `_.
+
.. tab:: Unix/macOS
.. code-block:: console
$ python -m pip list --format=json
- [{'name': 'colorama', 'version': '0.3.7'}, {'name': 'docopt', 'version': '0.6.2'}, ...
+ [
+ {'name': 'colorama', 'version': '0.3.7', 'metadata': {...}},
+ {'name': 'docopt', 'version': '0.6.2', 'metadata': {...}},
+ ...
+ ]
.. tab:: Windows
.. code-block:: console
C:\> py -m pip list --format=json
- [{'name': 'colorama', 'version': '0.3.7'}, {'name': 'docopt', 'version': '0.6.2'}, ...
+ [
+ {'name': 'colorama', 'version': '0.3.7', 'metadata': {...}},
+ {'name': 'docopt', 'version': '0.6.2', 'metadata': {...}},
+ ...
+ ]
#. Use freeze formatting
diff --git a/news/11097.feature.rst b/news/11097.feature.rst
new file mode 100644
index 00000000000..5f6cd512146
--- /dev/null
+++ b/news/11097.feature.rst
@@ -0,0 +1 @@
+Add a ``metadata`` field to the ``pip list`` JSON output.
diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py
index fc229efc242..dc48b883852 100644
--- a/src/pip/_internal/commands/list.py
+++ b/src/pip/_internal/commands/list.py
@@ -1,7 +1,17 @@
import json
import logging
from optparse import Values
-from typing import TYPE_CHECKING, Generator, List, Optional, Sequence, Tuple, cast
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Dict,
+ Generator,
+ List,
+ Optional,
+ Sequence,
+ Tuple,
+ cast,
+)
from pip._vendor.packaging.utils import canonicalize_name
@@ -344,7 +354,7 @@ def format_for_columns(
def format_for_json(packages: "_ProcessedDists", options: Values) -> str:
data = []
for dist in packages:
- info = {
+ info: Dict[str, Any] = {
"name": dist.raw_name,
"version": str(dist.version),
}
@@ -357,5 +367,6 @@ def format_for_json(packages: "_ProcessedDists", options: Values) -> str:
editable_project_location = dist.editable_project_location
if editable_project_location:
info["editable_project_location"] = editable_project_location
+ info["metadata"] = dist.metadata_dict
data.append(info)
return json.dumps(data)
diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py
index 94b8d8c1f72..73630803b75 100644
--- a/tests/functional/test_list.py
+++ b/tests/functional/test_list.py
@@ -1,6 +1,7 @@
import json
import os
from pathlib import Path
+from typing import Any, Dict
import pytest
@@ -35,6 +36,22 @@ def simple_script(
return script
+def subdict_in(subdict: Dict[Any, Any], d: Dict[Any, Any]) -> bool:
+ """Return true if all keys of subdict are in d and the correponding values match."""
+ return all(k in d and d[k] == v for k, v in subdict.items())
+
+
+def subdict_in_list(subdict: Dict[Any, Any], items: Any) -> bool:
+ """
+ Return true if at least one of the dictionaries in items contains all the keys
+ of subdict and the corresponding values match.
+ """
+ for item in items:
+ if subdict_in(subdict, item):
+ return True
+ return False
+
+
def test_basic_list(simple_script: PipTestEnvironment) -> None:
"""
Test default behavior of list command without format specifier.
@@ -97,7 +114,9 @@ def test_local_flag(simple_script: PipTestEnvironment) -> None:
"""
result = simple_script.pip("list", "--local", "--format=json")
- assert {"name": "simple", "version": "1.0"} in json.loads(result.stdout)
+ assert subdict_in_list(
+ {"name": "simple", "version": "1.0"}, json.loads(result.stdout)
+ )
def test_local_columns_flag(simple_script: PipTestEnvironment) -> None:
@@ -139,8 +158,12 @@ def test_user_flag(script: PipTestEnvironment, data: TestData) -> None:
script.pip("install", "-f", data.find_links, "--no-index", "simple==1.0")
script.pip("install", "-f", data.find_links, "--no-index", "--user", "simple2==2.0")
result = script.pip("list", "--user", "--format=json")
- assert {"name": "simple", "version": "1.0"} not in json.loads(result.stdout)
- assert {"name": "simple2", "version": "2.0"} in json.loads(result.stdout)
+ assert not subdict_in_list(
+ {"name": "simple", "version": "1.0"}, json.loads(result.stdout)
+ )
+ assert subdict_in_list(
+ {"name": "simple2", "version": "2.0"}, json.loads(result.stdout)
+ )
@pytest.mark.network
@@ -191,13 +214,19 @@ def test_uptodate_flag(script: PipTestEnvironment, data: TestData) -> None:
for item in json_output:
if "editable_project_location" in item:
item["editable_project_location"] = ""
- assert {"name": "simple", "version": "1.0"} not in json_output # 3.0 is latest
- assert {
- "name": "pip-test-package",
- "version": "0.1.1",
- "editable_project_location": "",
- } in json_output # editables included
- assert {"name": "simple2", "version": "3.0"} in json_output
+ assert not subdict_in_list(
+ {"name": "simple", "version": "1.0"},
+ json_output,
+ ) # 3.0 is latest
+ assert subdict_in_list(
+ {
+ "name": "pip-test-package",
+ "version": "0.1.1",
+ "editable_project_location": "",
+ },
+ json_output,
+ ) # editables included
+ assert subdict_in_list({"name": "simple2", "version": "3.0"}, json_output)
@pytest.mark.network
@@ -267,30 +296,33 @@ def test_outdated_flag(script: PipTestEnvironment, data: TestData) -> None:
for item in json_output:
if "editable_project_location" in item:
item["editable_project_location"] = ""
- assert {
- "name": "simple",
- "version": "1.0",
- "latest_version": "3.0",
- "latest_filetype": "sdist",
- } in json_output
- assert (
+ assert subdict_in_list(
+ {
+ "name": "simple",
+ "version": "1.0",
+ "latest_version": "3.0",
+ "latest_filetype": "sdist",
+ },
+ json_output,
+ )
+ assert subdict_in_list(
dict(
name="simplewheel",
version="1.0",
latest_version="2.0",
latest_filetype="wheel",
- )
- in json_output
+ ),
+ json_output,
)
- assert (
+ assert subdict_in_list(
dict(
name="pip-test-package",
version="0.1",
latest_version="0.1.1",
latest_filetype="sdist",
editable_project_location="",
- )
- in json_output
+ ),
+ json_output,
)
assert "simple2" not in {p["name"] for p in json_output}
@@ -358,7 +390,9 @@ def test_editables_flag(pip_test_package_script: PipTestEnvironment) -> None:
"""
result = pip_test_package_script.pip("list", "--editable", "--format=json")
result2 = pip_test_package_script.pip("list", "--editable")
- assert {"name": "simple", "version": "1.0"} not in json.loads(result.stdout)
+ assert not subdict_in_list(
+ {"name": "simple", "version": "1.0"}, json.loads(result.stdout)
+ )
assert os.path.join("src", "pip-test-package") in result2.stdout
@@ -368,7 +402,9 @@ def test_exclude_editable_flag(pip_test_package_script: PipTestEnvironment) -> N
Test the behavior of --editables flag in the list command
"""
result = pip_test_package_script.pip("list", "--exclude-editable", "--format=json")
- assert {"name": "simple", "version": "1.0"} in json.loads(result.stdout)
+ assert subdict_in_list(
+ {"name": "simple", "version": "1.0"}, json.loads(result.stdout)
+ )
assert "pip-test-package" not in {p["name"] for p in json.loads(result.stdout)}
@@ -516,7 +552,9 @@ def test_outdated_pre(script: PipTestEnvironment, data: TestData) -> None:
wheelhouse_path,
"--format=json",
)
- assert {"name": "simple", "version": "1.0"} in json.loads(result.stdout)
+ assert subdict_in_list(
+ {"name": "simple", "version": "1.0"}, json.loads(result.stdout)
+ )
result = script.pip(
"list",
"--no-index",
@@ -525,12 +563,15 @@ def test_outdated_pre(script: PipTestEnvironment, data: TestData) -> None:
"--outdated",
"--format=json",
)
- assert {
- "name": "simple",
- "version": "1.0",
- "latest_version": "1.1",
- "latest_filetype": "wheel",
- } in json.loads(result.stdout)
+ assert subdict_in_list(
+ {
+ "name": "simple",
+ "version": "1.0",
+ "latest_version": "1.1",
+ "latest_filetype": "wheel",
+ },
+ json.loads(result.stdout),
+ )
result_pre = script.pip(
"list",
"--no-index",
@@ -540,12 +581,15 @@ def test_outdated_pre(script: PipTestEnvironment, data: TestData) -> None:
"--pre",
"--format=json",
)
- assert {
- "name": "simple",
- "version": "1.0",
- "latest_version": "2.0.dev0",
- "latest_filetype": "wheel",
- } in json.loads(result_pre.stdout)
+ assert subdict_in_list(
+ {
+ "name": "simple",
+ "version": "1.0",
+ "latest_version": "2.0.dev0",
+ "latest_filetype": "wheel",
+ },
+ json.loads(result_pre.stdout),
+ )
def test_outdated_formats(script: PipTestEnvironment, data: TestData) -> None:
@@ -598,14 +642,16 @@ def test_outdated_formats(script: PipTestEnvironment, data: TestData) -> None:
"--format=json",
)
data = json.loads(result.stdout)
- assert data == [
+ assert len(data) == 1 # type: ignore
+ assert subdict_in_list(
{
"name": "simple",
"version": "1.0",
"latest_version": "1.1",
"latest_filetype": "wheel",
- }
- ]
+ },
+ data,
+ )
def test_not_required_flag(script: PipTestEnvironment, data: TestData) -> None:
@@ -634,8 +680,11 @@ def test_list_json(simple_script: PipTestEnvironment) -> None:
"""
result = simple_script.pip("list", "--format=json")
data = json.loads(result.stdout)
- assert {"name": "simple", "version": "1.0"} in data
- assert {"name": "simple2", "version": "3.0"} in data
+ assert subdict_in_list({"name": "simple", "version": "1.0"}, data)
+ assert subdict_in_list({"name": "simple2", "version": "3.0"}, data)
+ for item in data:
+ assert item["metadata"]["name"] == item["name"]
+ assert item["metadata"]["version"] == item["version"]
def test_list_path(tmpdir: Path, script: PipTestEnvironment, data: TestData) -> None:
@@ -644,12 +693,12 @@ def test_list_path(tmpdir: Path, script: PipTestEnvironment, data: TestData) ->
"""
result = script.pip("list", "--path", tmpdir, "--format=json")
json_result = json.loads(result.stdout)
- assert {"name": "simple", "version": "2.0"} not in json_result
+ assert not subdict_in_list({"name": "simple", "version": "2.0"}, json_result)
script.pip_install_local("--target", tmpdir, "simple==2.0")
result = script.pip("list", "--path", tmpdir, "--format=json")
json_result = json.loads(result.stdout)
- assert {"name": "simple", "version": "2.0"} in json_result
+ assert subdict_in_list({"name": "simple", "version": "2.0"}, json_result)
@pytest.mark.incompatible_with_test_venv
@@ -665,11 +714,11 @@ def test_list_path_exclude_user(
result = script.pip("list", "--user", "--format=json")
json_result = json.loads(result.stdout)
- assert {"name": "simple2", "version": "3.0"} in json_result
+ assert subdict_in_list({"name": "simple2", "version": "3.0"}, json_result)
result = script.pip("list", "--path", tmpdir, "--format=json")
json_result = json.loads(result.stdout)
- assert {"name": "simple", "version": "1.0"} in json_result
+ assert subdict_in_list({"name": "simple", "version": "1.0"}, json_result)
def test_list_path_multiple(
@@ -688,12 +737,12 @@ def test_list_path_multiple(
result = script.pip("list", "--path", path1, "--format=json")
json_result = json.loads(result.stdout)
- assert {"name": "simple", "version": "2.0"} in json_result
+ assert subdict_in_list({"name": "simple", "version": "2.0"}, json_result)
result = script.pip("list", "--path", path1, "--path", path2, "--format=json")
json_result = json.loads(result.stdout)
- assert {"name": "simple", "version": "2.0"} in json_result
- assert {"name": "simple2", "version": "3.0"} in json_result
+ assert subdict_in_list({"name": "simple", "version": "2.0"}, json_result)
+ assert subdict_in_list({"name": "simple2", "version": "3.0"}, json_result)
def test_list_skip_work_dir_pkg(script: PipTestEnvironment) -> None:
@@ -708,7 +757,7 @@ def test_list_skip_work_dir_pkg(script: PipTestEnvironment) -> None:
# List should not include package simple when run from package directory
result = script.pip("list", "--format=json", cwd=pkg_path)
json_result = json.loads(result.stdout)
- assert {"name": "simple", "version": "1.0"} not in json_result
+ assert not subdict_in_list({"name": "simple", "version": "1.0"}, json_result)
def test_list_include_work_dir_pkg(script: PipTestEnvironment) -> None:
@@ -727,7 +776,7 @@ def test_list_include_work_dir_pkg(script: PipTestEnvironment) -> None:
# when the package directory is in PYTHONPATH
result = script.pip("list", "--format=json", cwd=pkg_path)
json_result = json.loads(result.stdout)
- assert {"name": "simple", "version": "1.0"} in json_result
+ assert subdict_in_list({"name": "simple", "version": "1.0"}, json_result)
@pytest.mark.usefixtures("with_wheel")