diff --git a/news/13194.feature.rst b/news/13194.feature.rst new file mode 100644 index 00000000000..0ed40e3ab0b --- /dev/null +++ b/news/13194.feature.rst @@ -0,0 +1 @@ +Add a structured ``--json`` output to ``pip index versions`` diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index eeb7e651b79..73539345c3c 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -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", diff --git a/src/pip/_internal/commands/index.py b/src/pip/_internal/commands/index.py index 2e2661bba71..14f0feb6114 100644 --- a/src/pip/_internal/commands/index.py +++ b/src/pip/_internal/commands/index.py @@ -1,3 +1,4 @@ +import json import logging from optparse import Values from typing import Any, Iterable, List, Optional @@ -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, +) from pip._internal.exceptions import CommandError, DistributionNotFound, PipError from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder @@ -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()) @@ -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(latest, dist) diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 74b8d656b47..4f0f7866efd 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -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 @@ -111,9 +112,7 @@ 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(latest: str, dist: Optional[BaseDistribution]) -> None: if dist is not None: with indent_log(): if dist.version == latest: @@ -130,6 +129,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, @@ -163,7 +167,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(latest, dist) except UnicodeEncodeError: pass diff --git a/tests/functional/test_index.py b/tests/functional/test_index.py index 5a3c27bac9d..b58ad4a50d6 100644 --- a/tests/functional/test_index.py +++ b/tests/functional/test_index.py @@ -1,3 +1,5 @@ +import json + import pytest from pip._internal.cli.status_codes import ERROR, SUCCESS @@ -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: """