Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/13229.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Expand deprecation warning for wheels with non-standard file names.
6 changes: 1 addition & 5 deletions src/pip/_internal/index/package_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
12 changes: 9 additions & 3 deletions src/pip/_internal/metadata/importlib/_envs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)

Expand Down
107 changes: 64 additions & 43 deletions src/pip/_internal/models/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
"""

import re
from typing import Dict, Iterable, List
from typing import Dict, Iterable, List, Optional

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 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
Expand All @@ -18,54 +18,75 @@
class Wheel:
"""A wheel file"""

wheel_file_re = re.compile(
legacy_wheel_file_re = re.compile(
r"""^(?P<namever>(?P<name>[^\s-]+?)-(?P<ver>[^\s-]*?))
((-(?P<build>\d[^-]*?))?-(?P<pyver>[^\s-]+?)-(?P<abi>[^\s-]+?)-(?P<plat>[^\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
}

# 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: Optional[BuildTag] = 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

def get_formatted_file_tags(self) -> List[str]:
"""Return the wheel's tags as a sorted list of strings."""
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/test_install_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
29 changes: 19 additions & 10 deletions tests/unit/test_models_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,43 @@ 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
# see one, in this case the build tag = '4'
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")
with pytest.warns(deprecation.PipDeprecationWarning):
w = Wheel("simple-_invalid_-py2-none-any.whl")
assert w.version == "-invalid-"

def test_missing_version_raises(self) -> None:
Expand Down
Loading