diff --git a/news/13581.removal.rst b/news/13581.removal.rst new file mode 100644 index 00000000000..91c64fb50a1 --- /dev/null +++ b/news/13581.removal.rst @@ -0,0 +1 @@ +Remove support for non-standard legacy wheel filenames. diff --git a/src/pip/_internal/models/wheel.py b/src/pip/_internal/models/wheel.py index 60e97cb762b..fbd4902dc71 100644 --- a/src/pip/_internal/models/wheel.py +++ b/src/pip/_internal/models/wheel.py @@ -4,91 +4,30 @@ from __future__ import annotations -import re from collections.abc import Iterable from pip._vendor.packaging.tags import Tag -from pip._vendor.packaging.utils import BuildTag, parse_wheel_filename from pip._vendor.packaging.utils import ( InvalidWheelFilename as _PackagingInvalidWheelFilename, ) +from pip._vendor.packaging.utils import parse_wheel_filename from pip._internal.exceptions import InvalidWheelFilename -from pip._internal.utils.deprecation import deprecated class Wheel: """A wheel file""" - legacy_wheel_file_re = re.compile( - r"""^(?P(?P[^\s-]+?)-(?P[^\s-]*?)) - ((-(?P\d[^-]*?))?-(?P[^\s-]+?)-(?P[^\s-]+?)-(?P[^\s-]+?) - \.whl|\.dist-info)$""", - re.VERBOSE, - ) - def __init__(self, filename: str) -> None: self.filename = filename - # To make mypy happy specify type hints that can come from either - # parse_wheel_filename or the legacy_wheel_file_re match. - self.name: str - self._build_tag: BuildTag | None = None - try: wheel_info = parse_wheel_filename(filename) - self.name, _version, self._build_tag, self.file_tags = wheel_info - self.version = str(_version) except _PackagingInvalidWheelFilename as e: - # Check if the wheel filename is in the legacy format - legacy_wheel_info = self.legacy_wheel_file_re.match(filename) - if not legacy_wheel_info: - raise InvalidWheelFilename(e.args[0]) from None - - deprecated( - reason=( - f"Wheel filename {filename!r} is not correctly normalised. " - "Future versions of pip will raise the following error:\n" - f"{e.args[0]}\n\n" - ), - replacement=( - "to rename the wheel to use a correctly normalised " - "name (this may require updating the version in " - "the project metadata)" - ), - gone_in="25.3", - issue=12938, - ) - - self.name = legacy_wheel_info.group("name").replace("_", "-") - self.version = legacy_wheel_info.group("ver").replace("_", "-") - - # Generate the file tags from the legacy wheel filename - pyversions = legacy_wheel_info.group("pyver").split(".") - abis = legacy_wheel_info.group("abi").split(".") - plats = legacy_wheel_info.group("plat").split(".") - self.file_tags = frozenset( - Tag(interpreter=py, abi=abi, platform=plat) - for py in pyversions - for abi in abis - for plat in plats - ) - - @property - def build_tag(self) -> BuildTag: - if self._build_tag is not None: - return self._build_tag - - # Parse the build tag from the legacy wheel filename - legacy_wheel_info = self.legacy_wheel_file_re.match(self.filename) - assert legacy_wheel_info is not None, "guaranteed by filename validation" - build_tag = legacy_wheel_info.group("build") - match = re.match(r"^(\d+)(.*)$", build_tag) - assert match is not None, "guaranteed by filename validation" - build_tag_groups = match.groups() - self._build_tag = (int(build_tag_groups[0]), build_tag_groups[1]) - - return self._build_tag + raise InvalidWheelFilename(e.args[0]) from None + + self.name, _version, self.build_tag, self.file_tags = wheel_info + self.version = str(_version) def get_formatted_file_tags(self) -> list[str]: """Return the wheel's tags as a sorted list of strings.""" diff --git a/tests/functional/test_index_invalid_wheels.py b/tests/functional/test_index_invalid_wheels.py new file mode 100644 index 00000000000..73c83b08adb --- /dev/null +++ b/tests/functional/test_index_invalid_wheels.py @@ -0,0 +1,113 @@ +"""Test that pip index versions handles invalid (non-PEP 440) wheel filenames. + +This test was added for robustness after legacy wheel filename support +was removed in pip 25.3. +""" + +import json +import textwrap +from pathlib import Path + +from tests.lib import PipTestEnvironment +from tests.lib.wheel import make_wheel + + +def _create_test_index_with_invalid_wheels( + tmpdir: Path, package_name: str = "pkg" +) -> Path: + """Create a test index with both valid and invalid wheel filenames. + + Returns the path to the index directory. + """ + # Create test index + index_dir = tmpdir / "test_index" + index_dir.mkdir() + + (index_dir / "index.html").write_text( + textwrap.dedent( + f"""\ + + + {package_name} + + """ + ) + ) + + pkg_dir = index_dir / package_name + pkg_dir.mkdir() + + valid_wheels = [ + (f"{package_name}-1.0.0-py3-none-any.whl", "1.0.0"), + (f"{package_name}-2.0.0-py3-none-any.whl", "2.0.0"), + ] + invalid_wheels = [ + (f"{package_name}-3.0_1-py3-none-any.whl", "3.0"), # underscore in version + (f"{package_name}-_bad_-py3-none-any.wh", "0.0.0"), # no version + ( + f"{package_name}-5.0.0_build1-py3-none-any.whl", + "5.0.0", + ), # underscore in build tag + ] + + all_wheels = valid_wheels + invalid_wheels + for wheel_name, version in all_wheels: + wheel = make_wheel(name=package_name, version=version) + wheel.save_to(pkg_dir / wheel_name) + + # Create package index + links = [ + f'{wheel_name}
' for wheel_name, _ in all_wheels + ] + (pkg_dir / "index.html").write_text( + textwrap.dedent( + f"""\ + + + + {''.join(links)} + + + """ + ) + ) + + return index_dir + + +def test_index_versions_ignores_invalid_wheel_names( + script: PipTestEnvironment, + tmpdir: Path, +) -> None: + """Test that pip index versions ignores invalid wheel names.""" + index_dir = _create_test_index_with_invalid_wheels(tmpdir) + + # Run pip index versions with JSON output + result = script.pip( + "index", "versions", "pkg", "--json", "--index-url", index_dir.as_uri() + ) + + assert result.returncode == 0 + + output = json.loads(result.stdout) + assert output["name"] == "pkg" + assert output["latest"] == "2.0.0" + + expected_versions = ["2.0.0", "1.0.0"] + assert output["versions"] == expected_versions + + +def test_install_ignores_invalid_wheel_names( + script: PipTestEnvironment, + tmpdir: Path, +) -> None: + """Test that pip install ignores invalid wheel names and installs valid ones.""" + index_dir = _create_test_index_with_invalid_wheels(tmpdir) + + # Run pip install - should ignore invalid wheels and install the latest valid one + result = script.pip( + "install", "pkg", "--no-cache-dir", "--index-url", index_dir.as_uri() + ) + + assert result.returncode == 0 + script.assert_installed(pkg="2.0.0") diff --git a/tests/unit/test_models_wheel.py b/tests/unit/test_models_wheel.py index d9212ac8d67..a3a9cf8d140 100644 --- a/tests/unit/test_models_wheel.py +++ b/tests/unit/test_models_wheel.py @@ -4,7 +4,7 @@ from pip._internal.exceptions import InvalidWheelFilename from pip._internal.models.wheel import Wheel -from pip._internal.utils import compatibility_tags, deprecation +from pip._internal.utils import compatibility_tags class TestWheelFile: @@ -47,9 +47,8 @@ def test_single_digit_version(self) -> None: assert w.version == "1" def test_non_pep440_version(self) -> None: - with pytest.warns(deprecation.PipDeprecationWarning): - w = Wheel("simple-_invalid_-py2-none-any.whl") - assert w.version == "-invalid-" + with pytest.raises(InvalidWheelFilename): + Wheel("simple-_invalid_-py2-none-any.whl") def test_missing_version_raises(self) -> None: with pytest.raises(InvalidWheelFilename): @@ -254,16 +253,14 @@ def test_support_index_min__none_supported(self) -> None: def test_version_underscore_conversion(self) -> None: """ - Test that we convert '_' to '-' for versions parsed out of wheel - filenames + Test that underscore versions are now invalid (no longer converted) """ - with pytest.warns(deprecation.PipDeprecationWarning): - w = Wheel("simple-0.1_1-py2-none-any.whl") - assert w.version == "0.1-1" + with pytest.raises(InvalidWheelFilename): + Wheel("simple-0.1_1-py2-none-any.whl") - def test_invalid_wheel_warning(self) -> None: + def test_invalid_wheel_raises(self) -> None: """ - Test that wheel with invalid name produces warning + Test that wheel with invalid name now raises exception """ - with pytest.warns(deprecation.PipDeprecationWarning): + with pytest.raises(InvalidWheelFilename): Wheel("six-1.16.0_build1-py3-none-any.whl")