Skip to content

Commit c1100ee

Browse files
authored
Merge pull request #13194 from krishanbhasin-px/kb/f/pip-index-versions-json
Provide structured JSON output for `pip index versions`
2 parents 2014c69 + 37765ff commit c1100ee

File tree

5 files changed

+74
-8
lines changed

5 files changed

+74
-8
lines changed

news/13194.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a structured ``--json`` output to ``pip index versions``

src/pip/_internal/cli/cmdoptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,14 @@ def _handle_config_settings(
928928
"pip only finds stable versions.",
929929
)
930930

931+
json: Callable[..., Option] = partial(
932+
Option,
933+
"--json",
934+
action="store_true",
935+
default=False,
936+
help="Output data in a machine-readable JSON format.",
937+
)
938+
931939
disable_pip_version_check: Callable[..., Option] = partial(
932940
Option,
933941
"--disable-pip-version-check",

src/pip/_internal/commands/index.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import logging
23
from optparse import Values
34
from typing import Any, Iterable, List, Optional
@@ -7,7 +8,10 @@
78
from pip._internal.cli import cmdoptions
89
from pip._internal.cli.req_command import IndexGroupCommand
910
from pip._internal.cli.status_codes import ERROR, SUCCESS
10-
from pip._internal.commands.search import print_dist_installation_info
11+
from pip._internal.commands.search import (
12+
get_installed_distribution,
13+
print_dist_installation_info,
14+
)
1115
from pip._internal.exceptions import CommandError, DistributionNotFound, PipError
1216
from pip._internal.index.collector import LinkCollector
1317
from pip._internal.index.package_finder import PackageFinder
@@ -34,6 +38,7 @@ def add_options(self) -> None:
3438

3539
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
3640
self.cmd_opts.add_option(cmdoptions.pre())
41+
self.cmd_opts.add_option(cmdoptions.json())
3742
self.cmd_opts.add_option(cmdoptions.no_binary())
3843
self.cmd_opts.add_option(cmdoptions.only_binary())
3944

@@ -134,6 +139,21 @@ def get_available_package_versions(self, options: Values, args: List[Any]) -> No
134139
formatted_versions = [str(ver) for ver in sorted(versions, reverse=True)]
135140
latest = formatted_versions[0]
136141

137-
write_output(f"{query} ({latest})")
138-
write_output("Available versions: {}".format(", ".join(formatted_versions)))
139-
print_dist_installation_info(query, latest)
142+
dist = get_installed_distribution(query)
143+
144+
if options.json:
145+
structured_output = {
146+
"name": query,
147+
"versions": formatted_versions,
148+
"latest": latest,
149+
}
150+
151+
if dist is not None:
152+
structured_output["installed_version"] = str(dist.version)
153+
154+
write_output(json.dumps(structured_output))
155+
156+
else:
157+
write_output(f"{query} ({latest})")
158+
write_output("Available versions: {}".format(", ".join(formatted_versions)))
159+
print_dist_installation_info(latest, dist)

src/pip/_internal/commands/search.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS
1515
from pip._internal.exceptions import CommandError
1616
from pip._internal.metadata import get_default_environment
17+
from pip._internal.metadata.base import BaseDistribution
1718
from pip._internal.models.index import PyPI
1819
from pip._internal.network.xmlrpc import PipXmlrpcTransport
1920
from pip._internal.utils.logging import indent_log
@@ -110,9 +111,7 @@ def transform_hits(hits: List[Dict[str, str]]) -> List["TransformedHit"]:
110111
return list(packages.values())
111112

112113

113-
def print_dist_installation_info(name: str, latest: str) -> None:
114-
env = get_default_environment()
115-
dist = env.get_distribution(name)
114+
def print_dist_installation_info(latest: str, dist: Optional[BaseDistribution]) -> None:
116115
if dist is not None:
117116
with indent_log():
118117
if dist.version == latest:
@@ -129,6 +128,11 @@ def print_dist_installation_info(name: str, latest: str) -> None:
129128
write_output("LATEST: %s", latest)
130129

131130

131+
def get_installed_distribution(name: str) -> Optional[BaseDistribution]:
132+
env = get_default_environment()
133+
return env.get_distribution(name)
134+
135+
132136
def print_results(
133137
hits: List["TransformedHit"],
134138
name_column_width: Optional[int] = None,
@@ -162,7 +166,8 @@ def print_results(
162166
line = f"{name_latest:{name_column_width}} - {summary}"
163167
try:
164168
write_output(line)
165-
print_dist_installation_info(name, latest)
169+
dist = get_installed_distribution(name)
170+
print_dist_installation_info(latest, dist)
166171
except UnicodeEncodeError:
167172
pass
168173

tests/functional/test_index.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import json
2+
13
import pytest
24

35
from pip._internal.cli.status_codes import ERROR, SUCCESS
@@ -6,6 +8,36 @@
68
from tests.lib import PipTestEnvironment
79

810

11+
@pytest.mark.network
12+
def test_json_structured_output(script: PipTestEnvironment) -> None:
13+
"""
14+
Test that --json flag returns structured output
15+
"""
16+
output = script.pip("index", "versions", "pip", "--json", allow_stderr_warning=True)
17+
structured_output = json.loads(output.stdout)
18+
19+
assert isinstance(structured_output, dict)
20+
assert "name" in structured_output
21+
assert structured_output["name"] == "pip"
22+
assert "latest" in structured_output
23+
assert isinstance(structured_output["latest"], str)
24+
assert "versions" in structured_output
25+
assert isinstance(structured_output["versions"], list)
26+
assert (
27+
"20.2.3, 20.2.2, 20.2.1, 20.2, 20.1.1, 20.1, 20.0.2"
28+
", 20.0.1, 19.3.1, 19.3, 19.2.3, 19.2.2, 19.2.1, 19.2, 19.1.1"
29+
", 19.1, 19.0.3, 19.0.2, 19.0.1, 19.0, 18.1, 18.0, 10.0.1, 10.0.0, "
30+
"9.0.3, 9.0.2, 9.0.1, 9.0.0, 8.1.2, 8.1.1, "
31+
"8.1.0, 8.0.3, 8.0.2, 8.0.1, 8.0.0, 7.1.2, 7.1.1, 7.1.0, 7.0.3, "
32+
"7.0.2, 7.0.1, 7.0.0, 6.1.1, 6.1.0, 6.0.8, 6.0.7, 6.0.6, 6.0.5, "
33+
"6.0.4, 6.0.3, 6.0.2, 6.0.1, 6.0, 1.5.6, 1.5.5, 1.5.4, 1.5.3, "
34+
"1.5.2, 1.5.1, 1.5, 1.4.1, 1.4, 1.3.1, 1.3, 1.2.1, 1.2, 1.1, 1.0.2,"
35+
" 1.0.1, 1.0, 0.8.3, 0.8.2, 0.8.1, 0.8, 0.7.2, 0.7.1, 0.7, 0.6.3, "
36+
"0.6.2, 0.6.1, 0.6, 0.5.1, 0.5, 0.4, 0.3.1, "
37+
"0.3, 0.2.1, 0.2" in ", ".join(structured_output["versions"])
38+
)
39+
40+
941
@pytest.mark.network
1042
def test_list_all_versions_basic_search(script: PipTestEnvironment) -> None:
1143
"""

0 commit comments

Comments
 (0)