diff --git a/news/12917.bugfix.rst b/news/12917.bugfix.rst new file mode 100644 index 00000000000..4c937c85079 --- /dev/null +++ b/news/12917.bugfix.rst @@ -0,0 +1 @@ +Only accept standard PEP 440 wheel filenames. diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 85628ee5d7a..ce0842b07e6 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -514,11 +514,7 @@ def _sort_key(self, candidate: InstallationCandidate) -> CandidateSortingKey: ) if self._prefer_binary: binary_preference = 1 - if wheel.build_tag is not None: - match = re.match(r"^(\d+)(.*)$", wheel.build_tag) - assert match is not None, "guaranteed by filename validation" - build_tag_groups = match.groups() - build_tag = (int(build_tag_groups[0]), build_tag_groups[1]) + build_tag = wheel.build_tag else: # sdist pri = -(support_num) has_allowed_hash = int(link.is_hash_allowed(self._hashes)) diff --git a/src/pip/_internal/metadata/importlib/_envs.py b/src/pip/_internal/metadata/importlib/_envs.py index 4d906fd3149..681fd241fdc 100644 --- a/src/pip/_internal/metadata/importlib/_envs.py +++ b/src/pip/_internal/metadata/importlib/_envs.py @@ -8,10 +8,14 @@ import zipimport from typing import Iterator, List, Optional, Sequence, Set, Tuple -from pip._vendor.packaging.utils import NormalizedName, canonicalize_name +from pip._vendor.packaging.utils import ( + InvalidWheelFilename, + NormalizedName, + canonicalize_name, + parse_wheel_filename, +) from pip._internal.metadata.base import BaseDistribution, BaseEnvironment -from pip._internal.models.wheel import Wheel from pip._internal.utils.deprecation import deprecated from pip._internal.utils.filetypes import WHEEL_EXTENSION @@ -26,7 +30,9 @@ def _looks_like_wheel(location: str) -> bool: return False if not os.path.isfile(location): return False - if not Wheel.wheel_file_re.match(os.path.basename(location)): + try: + parse_wheel_filename(os.path.basename(location)) + except InvalidWheelFilename: return False return zipfile.is_zipfile(location) diff --git a/src/pip/_internal/models/wheel.py b/src/pip/_internal/models/wheel.py index ea8560089d3..ea2f446823a 100644 --- a/src/pip/_internal/models/wheel.py +++ b/src/pip/_internal/models/wheel.py @@ -2,70 +2,29 @@ name that have meaning. """ -import re from typing import Dict, Iterable, List from pip._vendor.packaging.tags import Tag from pip._vendor.packaging.utils import ( - InvalidWheelFilename as PackagingInvalidWheelName, + 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""" - 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: - """ - :raises InvalidWheelFilename: when the filename is invalid for a wheel - """ - wheel_info = self.wheel_file_re.match(filename) - if not wheel_info: - raise InvalidWheelFilename(f"{filename} is not a valid wheel filename.") self.filename = filename - self.name = wheel_info.group("name").replace("_", "-") - _version = wheel_info.group("ver") - if "_" in _version: - try: - parse_wheel_filename(filename) - except PackagingInvalidWheelName as e: - 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.1", - issue=12938, - ) - - _version = _version.replace("_", "-") - - self.version = _version - self.build_tag = wheel_info.group("build") - self.pyversions = wheel_info.group("pyver").split(".") - self.abis = wheel_info.group("abi").split(".") - self.plats = wheel_info.group("plat").split(".") - - # All the tag combinations from this file - self.file_tags = { - Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats - } + try: + wheel_info = parse_wheel_filename(filename) + except _PackagingInvalidWheelFilename as e: + 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_install_wheel.py b/tests/functional/test_install_wheel.py index 7e7aeaf7a81..e01ecfb57f3 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -190,7 +190,7 @@ def test_install_from_wheel_with_headers(script: PipTestEnvironment) -> None: dist_info_folder = script.site_packages / "headers.dist-0.1.dist-info" result.did_create(dist_info_folder) - header_scheme_path = get_header_scheme_path_for_script(script, "headers.dist") + header_scheme_path = get_header_scheme_path_for_script(script, "headers-dist") header_path = header_scheme_path / "header.h" assert header_path.read_text() == header_text diff --git a/tests/unit/test_models_wheel.py b/tests/unit/test_models_wheel.py index e87b2c107a0..e77fb245dd4 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: @@ -12,17 +12,24 @@ def test_std_wheel_pattern(self) -> None: w = Wheel("simple-1.1.1-py2-none-any.whl") assert w.name == "simple" assert w.version == "1.1.1" - assert w.pyversions == ["py2"] - assert w.abis == ["none"] - assert w.plats == ["any"] + assert w.build_tag == () + assert w.file_tags == frozenset( + [Tag(interpreter="py2", abi="none", platform="any")] + ) def test_wheel_pattern_multi_values(self) -> None: w = Wheel("simple-1.1-py2.py3-abi1.abi2-any.whl") assert w.name == "simple" assert w.version == "1.1" - assert w.pyversions == ["py2", "py3"] - assert w.abis == ["abi1", "abi2"] - assert w.plats == ["any"] + assert w.build_tag == () + assert w.file_tags == frozenset( + [ + Tag(interpreter="py2", abi="abi1", platform="any"), + Tag(interpreter="py2", abi="abi2", platform="any"), + Tag(interpreter="py3", abi="abi1", platform="any"), + Tag(interpreter="py3", abi="abi2", platform="any"), + ] + ) def test_wheel_with_build_tag(self) -> None: # pip doesn't do anything with build tags, but theoretically, we might @@ -30,17 +37,18 @@ def test_wheel_with_build_tag(self) -> None: w = Wheel("simple-1.1-4-py2-none-any.whl") assert w.name == "simple" assert w.version == "1.1" - assert w.pyversions == ["py2"] - assert w.abis == ["none"] - assert w.plats == ["any"] + assert w.build_tag == (4, "") + assert w.file_tags == frozenset( + [Tag(interpreter="py2", abi="none", platform="any")] + ) def test_single_digit_version(self) -> None: w = Wheel("simple-1-py2-none-any.whl") assert w.version == "1" def test_non_pep440_version(self) -> None: - 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): @@ -195,16 +203,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 + '_' is not PEP 440 compliant in the version part of a wheel filename """ - 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: """ Test that wheel with invalid name produces warning """ - with pytest.warns(deprecation.PipDeprecationWarning): + with pytest.raises(InvalidWheelFilename): Wheel("six-1.16.0_build1-py3-none-any.whl")