From 40187ef5ee41d43462050a7c31dc2b7363686ae6 Mon Sep 17 00:00:00 2001 From: Krishan Bhasin <164889026+krishanbhasin-px@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:24:56 +0000 Subject: [PATCH 1/9] Add the outline for structured output --- src/pip/_internal/cli/cmdoptions.py | 8 ++++++++ src/pip/_internal/commands/index.py | 19 +++++++++++++++++++ tests/functional/test_index.py | 26 ++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) 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..9620a1f6eae 100644 --- a/src/pip/_internal/commands/index.py +++ b/src/pip/_internal/commands/index.py @@ -1,7 +1,9 @@ +import json import logging from optparse import Values from typing import Any, Iterable, List, Optional +from pip._internal.metadata import get_default_environment from pip._vendor.packaging.version import Version from pip._internal.cli import cmdoptions @@ -34,6 +36,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 +137,22 @@ 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] + if options.json: + env = get_default_environment() + dist = env.get_distribution(query) + structured_output = { + "name": query, + "versions": formatted_versions, + "latest": latest, + } + + if dist is not None: + structured_output["installed_version"] = dist.version + + + write_output(json.dumps(structured_output)) + return + write_output(f"{query} ({latest})") write_output("Available versions: {}".format(", ".join(formatted_versions))) print_dist_installation_info(query, latest) diff --git a/tests/functional/test_index.py b/tests/functional/test_index.py index 5a3c27bac9d..6566ddfceae 100644 --- a/tests/functional/test_index.py +++ b/tests/functional/test_index.py @@ -1,3 +1,4 @@ +import json import pytest from pip._internal.cli.status_codes import ERROR, SUCCESS @@ -6,6 +7,31 @@ 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 "name" in structured_output + assert "versions" in structured_output + assert "latest" in structured_output + 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 structured_output.get("versions") + ) + @pytest.mark.network def test_list_all_versions_basic_search(script: PipTestEnvironment) -> None: """ From 33d7de4f9043b8524c5890bb81a12f0ee8cedba1 Mon Sep 17 00:00:00 2001 From: Krishan Bhasin <8904718+KrishanBhasin@users.noreply.github.com> Date: Thu, 30 Jan 2025 21:58:57 +0000 Subject: [PATCH 2/9] fixup! Add the outline for structured output --- src/pip/_internal/commands/index.py | 2 +- tests/functional/test_index.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/index.py b/src/pip/_internal/commands/index.py index 9620a1f6eae..237af495713 100644 --- a/src/pip/_internal/commands/index.py +++ b/src/pip/_internal/commands/index.py @@ -147,7 +147,7 @@ def get_available_package_versions(self, options: Values, args: List[Any]) -> No } if dist is not None: - structured_output["installed_version"] = dist.version + structured_output["installed_version"] = str(dist.version) write_output(json.dumps(structured_output)) diff --git a/tests/functional/test_index.py b/tests/functional/test_index.py index 6566ddfceae..3edb17bf17e 100644 --- a/tests/functional/test_index.py +++ b/tests/functional/test_index.py @@ -29,7 +29,7 @@ def test_json_structured_output(script: PipTestEnvironment) -> None: "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 structured_output.get("versions") + "0.3, 0.2.1, 0.2" in ", ".join(structured_output.get("versions")) ) @pytest.mark.network From 52a76a55e57f1169b9eff3c40327a3d285d32457 Mon Sep 17 00:00:00 2001 From: Krishan Bhasin <8904718+KrishanBhasin@users.noreply.github.com> Date: Thu, 30 Jan 2025 21:59:08 +0000 Subject: [PATCH 3/9] lint --- src/pip/_internal/commands/index.py | 11 +++++------ tests/functional/test_index.py | 2 ++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/commands/index.py b/src/pip/_internal/commands/index.py index 237af495713..47f38179999 100644 --- a/src/pip/_internal/commands/index.py +++ b/src/pip/_internal/commands/index.py @@ -3,7 +3,6 @@ from optparse import Values from typing import Any, Iterable, List, Optional -from pip._internal.metadata import get_default_environment from pip._vendor.packaging.version import Version from pip._internal.cli import cmdoptions @@ -13,6 +12,7 @@ from pip._internal.exceptions import CommandError, DistributionNotFound, PipError from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder +from pip._internal.metadata import get_default_environment from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython from pip._internal.network.session import PipSession @@ -141,15 +141,14 @@ def get_available_package_versions(self, options: Values, args: List[Any]) -> No env = get_default_environment() dist = env.get_distribution(query) structured_output = { - "name": query, - "versions": formatted_versions, - "latest": latest, - } + "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)) return diff --git a/tests/functional/test_index.py b/tests/functional/test_index.py index 3edb17bf17e..2d60ba21bfe 100644 --- a/tests/functional/test_index.py +++ b/tests/functional/test_index.py @@ -1,4 +1,5 @@ import json + import pytest from pip._internal.cli.status_codes import ERROR, SUCCESS @@ -32,6 +33,7 @@ def test_json_structured_output(script: PipTestEnvironment) -> None: "0.3, 0.2.1, 0.2" in ", ".join(structured_output.get("versions")) ) + @pytest.mark.network def test_list_all_versions_basic_search(script: PipTestEnvironment) -> None: """ From 47f94de1fe81d00cfc1d9eec1653bb0e53e6f0d2 Mon Sep 17 00:00:00 2001 From: Krishan Bhasin <8904718+KrishanBhasin@users.noreply.github.com> Date: Thu, 30 Jan 2025 22:15:59 +0000 Subject: [PATCH 4/9] add news entry --- news/13194.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/13194.feature.rst diff --git a/news/13194.feature.rst b/news/13194.feature.rst new file mode 100644 index 00000000000..f8141cf08ce --- /dev/null +++ b/news/13194.feature.rst @@ -0,0 +1 @@ +Add a structured `--json` output to `pip index versions` From 88d748b4fcac8d2a2b0d64ed3e5495c15282c3c3 Mon Sep 17 00:00:00 2001 From: Krishan Bhasin <8904718+KrishanBhasin@users.noreply.github.com> Date: Thu, 30 Jan 2025 22:21:10 +0000 Subject: [PATCH 5/9] fixup! add news entry --- news/13194.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/13194.feature.rst b/news/13194.feature.rst index f8141cf08ce..0ed40e3ab0b 100644 --- a/news/13194.feature.rst +++ b/news/13194.feature.rst @@ -1 +1 @@ -Add a structured `--json` output to `pip index versions` +Add a structured ``--json`` output to ``pip index versions`` From f3548985fd7ffce6637bd87daab553605c3c2d1d Mon Sep 17 00:00:00 2001 From: Krishan Bhasin <8904718+KrishanBhasin@users.noreply.github.com> Date: Thu, 30 Jan 2025 22:41:19 +0000 Subject: [PATCH 6/9] fixup! fixup! Add the outline for structured output --- tests/functional/test_index.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_index.py b/tests/functional/test_index.py index 2d60ba21bfe..b58ad4a50d6 100644 --- a/tests/functional/test_index.py +++ b/tests/functional/test_index.py @@ -16,9 +16,13 @@ def test_json_structured_output(script: PipTestEnvironment) -> None: 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 "versions" 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" @@ -30,7 +34,7 @@ def test_json_structured_output(script: PipTestEnvironment) -> None: "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.get("versions")) + "0.3, 0.2.1, 0.2" in ", ".join(structured_output["versions"]) ) From 50f72f22329ae89de3416b00d730d9bbb7ed5931 Mon Sep 17 00:00:00 2001 From: Krishan Bhasin <8904718+KrishanBhasin@users.noreply.github.com> Date: Wed, 5 Feb 2025 20:16:40 +0000 Subject: [PATCH 7/9] refactor if/else block --- src/pip/_internal/commands/index.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/commands/index.py b/src/pip/_internal/commands/index.py index 47f38179999..5c0c41175c9 100644 --- a/src/pip/_internal/commands/index.py +++ b/src/pip/_internal/commands/index.py @@ -150,8 +150,8 @@ def get_available_package_versions(self, options: Values, args: List[Any]) -> No structured_output["installed_version"] = str(dist.version) write_output(json.dumps(structured_output)) - return - write_output(f"{query} ({latest})") - write_output("Available versions: {}".format(", ".join(formatted_versions))) - print_dist_installation_info(query, latest) + else: + write_output(f"{query} ({latest})") + write_output("Available versions: {}".format(", ".join(formatted_versions))) + print_dist_installation_info(query, latest) From d31e8057ecb7323c11f6216c5cd668d9f1a2a726 Mon Sep 17 00:00:00 2001 From: Krishan Bhasin <8904718+KrishanBhasin@users.noreply.github.com> Date: Wed, 5 Feb 2025 21:45:28 +0000 Subject: [PATCH 8/9] avoid duplication of dist lookup --- src/pip/_internal/commands/index.py | 12 +++++++----- src/pip/_internal/commands/search.py | 15 +++++++++++---- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/commands/index.py b/src/pip/_internal/commands/index.py index 5c0c41175c9..61e57fddb80 100644 --- a/src/pip/_internal/commands/index.py +++ b/src/pip/_internal/commands/index.py @@ -8,11 +8,13 @@ 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 -from pip._internal.metadata import get_default_environment from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython from pip._internal.network.session import PipSession @@ -137,9 +139,9 @@ 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] + dist = get_installed_distribution(query) + if options.json: - env = get_default_environment() - dist = env.get_distribution(query) structured_output = { "name": query, "versions": formatted_versions, @@ -154,4 +156,4 @@ def get_available_package_versions(self, options: Values, args: List[Any]) -> No else: write_output(f"{query} ({latest})") write_output("Available versions: {}".format(", ".join(formatted_versions))) - print_dist_installation_info(query, latest) + print_dist_installation_info_if_exists(latest, dist) diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 74b8d656b47..a42927d4198 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,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: @@ -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, @@ -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 From 37765ffb9f9e0997622ed0497bcaa7519273c158 Mon Sep 17 00:00:00 2001 From: Krishan Bhasin <8904718+KrishanBhasin@users.noreply.github.com> Date: Thu, 6 Feb 2025 07:09:23 +0000 Subject: [PATCH 9/9] fixup! avoid duplication of dist lookup --- src/pip/_internal/commands/index.py | 4 ++-- src/pip/_internal/commands/search.py | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/commands/index.py b/src/pip/_internal/commands/index.py index 61e57fddb80..14f0feb6114 100644 --- a/src/pip/_internal/commands/index.py +++ b/src/pip/_internal/commands/index.py @@ -10,7 +10,7 @@ from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.commands.search import ( get_installed_distribution, - print_dist_installation_info_if_exists, + print_dist_installation_info, ) from pip._internal.exceptions import CommandError, DistributionNotFound, PipError from pip._internal.index.collector import LinkCollector @@ -156,4 +156,4 @@ def get_available_package_versions(self, options: Values, args: List[Any]) -> No else: write_output(f"{query} ({latest})") write_output("Available versions: {}".format(", ".join(formatted_versions))) - print_dist_installation_info_if_exists(latest, dist) + print_dist_installation_info(latest, dist) diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index a42927d4198..4f0f7866efd 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -112,9 +112,7 @@ def transform_hits(hits: List[Dict[str, str]]) -> List["TransformedHit"]: return list(packages.values()) -def print_dist_installation_info_if_exists( - latest: str, dist: Optional[BaseDistribution] -) -> None: +def print_dist_installation_info(latest: str, dist: Optional[BaseDistribution]) -> None: if dist is not None: with indent_log(): if dist.version == latest: @@ -170,7 +168,7 @@ def print_results( try: write_output(line) dist = get_installed_distribution(name) - print_dist_installation_info_if_exists(latest, dist) + print_dist_installation_info(latest, dist) except UnicodeEncodeError: pass