Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]

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))

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(
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