From ac6533e084b38a3ab8689c6611aaf792765bee1e Mon Sep 17 00:00:00 2001 From: Richard Si Date: Mon, 21 Apr 2025 12:53:18 -0400 Subject: [PATCH] Handle invalid versions gracefully for all list formats --- news/13345.bugfix.rst | 2 ++ src/pip/_internal/commands/list.py | 18 ++++++++++++------ .../test_invalid_versions_and_specifiers.py | 7 +++++-- 3 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 news/13345.bugfix.rst diff --git a/news/13345.bugfix.rst b/news/13345.bugfix.rst new file mode 100644 index 00000000000..85f31ec1592 --- /dev/null +++ b/news/13345.bugfix.rst @@ -0,0 +1,2 @@ +``pip list`` with the ``json`` or ``freeze`` format enabled will no longer +crash when encountering a package with an invalid version. diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index b03085070d2..164b7349f41 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Generator, List, Optional, Sequence, Tuple, cast from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.packaging.version import Version +from pip._vendor.packaging.version import InvalidVersion, Version from pip._internal.cli import cmdoptions from pip._internal.cli.index_command import IndexGroupCommand @@ -285,12 +285,14 @@ def output_package_listing( self.output_package_listing_columns(data, header) elif options.list_format == "freeze": for dist in packages: + try: + req_string = f"{dist.raw_name}=={dist.version}" + except InvalidVersion: + req_string = f"{dist.raw_name}==={dist.raw_version}" if options.verbose >= 1: - write_output( - "%s==%s (%s)", dist.raw_name, dist.version, dist.location - ) + write_output("%s (%s)", req_string, dist.location) else: - write_output("%s==%s", dist.raw_name, dist.version) + write_output(req_string) elif options.list_format == "json": write_output(format_for_json(packages, options)) @@ -374,9 +376,13 @@ def wheel_build_tag(dist: BaseDistribution) -> Optional[str]: def format_for_json(packages: "_ProcessedDists", options: Values) -> str: data = [] for dist in packages: + try: + version = str(dist.version) + except InvalidVersion: + version = dist.raw_version info = { "name": dist.raw_name, - "version": str(dist.version), + "version": version, } if options.verbose >= 1: info["location"] = dist.location or "" diff --git a/tests/functional/test_invalid_versions_and_specifiers.py b/tests/functional/test_invalid_versions_and_specifiers.py index 6349036bf52..07dff104f8b 100644 --- a/tests/functional/test_invalid_versions_and_specifiers.py +++ b/tests/functional/test_invalid_versions_and_specifiers.py @@ -96,12 +96,15 @@ def test_upgrade_require_invalid_version( script.pip("install", "--index-url", index_url, "require-invalid-version") -def test_list_invalid_version(script: PipTestEnvironment, data: TestData) -> None: +@pytest.mark.parametrize("format", ["columns", "freeze", "json"]) +def test_list_invalid_version( + script: PipTestEnvironment, data: TestData, format: str +) -> None: """ Test that pip can list an environment containing a package with a legacy version. """ _install_invalid_version(script, data) - script.pip("list") + script.pip("list", f"--format={format}") def test_freeze_invalid_version(script: PipTestEnvironment, data: TestData) -> None: