Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide structured JSON output for pip index versions #13194

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions news/13194.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a structured ``--json`` output to ``pip index versions``
8 changes: 8 additions & 0 deletions src/pip/_internal/cli/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,14 @@ def _handle_config_settings(
"pip only finds stable versions.",
)

json: Callable[..., Option] = partial(
Option,
"--json",
action="store_true",
default=False,
help="Output data in a machine-readable JSON format.",
)

disable_pip_version_check: Callable[..., Option] = partial(
Option,
"--disable-pip-version-check",
Expand Down
28 changes: 24 additions & 4 deletions src/pip/_internal/commands/index.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import logging
from optparse import Values
from typing import Any, Iterable, List, Optional
Expand All @@ -7,7 +8,10 @@
from pip._internal.cli import cmdoptions
from pip._internal.cli.req_command import IndexGroupCommand
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.commands.search import print_dist_installation_info
from pip._internal.commands.search import (
get_installed_distribution,
print_dist_installation_info_if_exists,
)
from pip._internal.exceptions import CommandError, DistributionNotFound, PipError
from pip._internal.index.collector import LinkCollector
from pip._internal.index.package_finder import PackageFinder
Expand All @@ -34,6 +38,7 @@ def add_options(self) -> None:

self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
self.cmd_opts.add_option(cmdoptions.pre())
self.cmd_opts.add_option(cmdoptions.json())
self.cmd_opts.add_option(cmdoptions.no_binary())
self.cmd_opts.add_option(cmdoptions.only_binary())

Expand Down Expand Up @@ -134,6 +139,21 @@ def get_available_package_versions(self, options: Values, args: List[Any]) -> No
formatted_versions = [str(ver) for ver in sorted(versions, reverse=True)]
latest = formatted_versions[0]
pradyunsg marked this conversation as resolved.
Show resolved Hide resolved

write_output(f"{query} ({latest})")
write_output("Available versions: {}".format(", ".join(formatted_versions)))
print_dist_installation_info(query, latest)
dist = get_installed_distribution(query)

if options.json:
structured_output = {
"name": query,
"versions": formatted_versions,
"latest": latest,
}

if dist is not None:
structured_output["installed_version"] = str(dist.version)

write_output(json.dumps(structured_output))

KrishanBhasin marked this conversation as resolved.
Show resolved Hide resolved
else:
write_output(f"{query} ({latest})")
write_output("Available versions: {}".format(", ".join(formatted_versions)))
print_dist_installation_info_if_exists(latest, dist)
15 changes: 11 additions & 4 deletions src/pip/_internal/commands/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS
from pip._internal.exceptions import CommandError
from pip._internal.metadata import get_default_environment
from pip._internal.metadata.base import BaseDistribution
from pip._internal.models.index import PyPI
from pip._internal.network.xmlrpc import PipXmlrpcTransport
from pip._internal.utils.logging import indent_log
Expand Down Expand Up @@ -111,9 +112,9 @@ def transform_hits(hits: List[Dict[str, str]]) -> List["TransformedHit"]:
return list(packages.values())


def print_dist_installation_info(name: str, latest: str) -> None:
env = get_default_environment()
dist = env.get_distribution(name)
def print_dist_installation_info_if_exists(
KrishanBhasin marked this conversation as resolved.
Show resolved Hide resolved
latest: str, dist: Optional[BaseDistribution]
) -> None:
if dist is not None:
with indent_log():
if dist.version == latest:
Expand All @@ -130,6 +131,11 @@ def print_dist_installation_info(name: str, latest: str) -> None:
write_output("LATEST: %s", latest)


def get_installed_distribution(name: str) -> Optional[BaseDistribution]:
env = get_default_environment()
return env.get_distribution(name)


def print_results(
hits: List["TransformedHit"],
name_column_width: Optional[int] = None,
Expand Down Expand Up @@ -163,7 +169,8 @@ def print_results(
line = f"{name_latest:{name_column_width}} - {summary}"
try:
write_output(line)
print_dist_installation_info(name, latest)
dist = get_installed_distribution(name)
print_dist_installation_info_if_exists(latest, dist)
except UnicodeEncodeError:
pass

Expand Down
32 changes: 32 additions & 0 deletions tests/functional/test_index.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json

import pytest

from pip._internal.cli.status_codes import ERROR, SUCCESS
Expand All @@ -6,6 +8,36 @@
from tests.lib import PipTestEnvironment


@pytest.mark.network
def test_json_structured_output(script: PipTestEnvironment) -> None:
"""
Test that --json flag returns structured output
"""
output = script.pip("index", "versions", "pip", "--json", allow_stderr_warning=True)
structured_output = json.loads(output.stdout)

assert isinstance(structured_output, dict)
assert "name" in structured_output
assert structured_output["name"] == "pip"
assert "latest" in structured_output
assert isinstance(structured_output["latest"], str)
assert "versions" in structured_output
assert isinstance(structured_output["versions"], list)
assert (
"20.2.3, 20.2.2, 20.2.1, 20.2, 20.1.1, 20.1, 20.0.2"
", 20.0.1, 19.3.1, 19.3, 19.2.3, 19.2.2, 19.2.1, 19.2, 19.1.1"
", 19.1, 19.0.3, 19.0.2, 19.0.1, 19.0, 18.1, 18.0, 10.0.1, 10.0.0, "
"9.0.3, 9.0.2, 9.0.1, 9.0.0, 8.1.2, 8.1.1, "
"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, "
"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, "
"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, "
"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,"
" 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, "
"0.6.2, 0.6.1, 0.6, 0.5.1, 0.5, 0.4, 0.3.1, "
"0.3, 0.2.1, 0.2" in ", ".join(structured_output["versions"])
)


@pytest.mark.network
def test_list_all_versions_basic_search(script: PipTestEnvironment) -> None:
"""
Expand Down
Loading