Skip to content

Commit c3f08f0

Browse files
authored
Use parse_wheel_filename to parse wheel filename, with fallback
2 parents 0d4ed13 + b49dd3a commit c3f08f0

File tree

6 files changed

+98
-62
lines changed

6 files changed

+98
-62
lines changed

news/13229.bugfix.rst

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Parse wheel filenames according to `binary distribution format specification
2+
<https://packaging.python.org/en/latest/specifications/binary-distribution-format/#file-format>`_.
3+
When a filename doesn't match the spec a deprecation warning is emitted and the
4+
filename is parsed using the old method.

src/pip/_internal/index/package_finder.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -514,11 +514,7 @@ def _sort_key(self, candidate: InstallationCandidate) -> CandidateSortingKey:
514514
)
515515
if self._prefer_binary:
516516
binary_preference = 1
517-
if wheel.build_tag is not None:
518-
match = re.match(r"^(\d+)(.*)$", wheel.build_tag)
519-
assert match is not None, "guaranteed by filename validation"
520-
build_tag_groups = match.groups()
521-
build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
517+
build_tag = wheel.build_tag
522518
else: # sdist
523519
pri = -(support_num)
524520
has_allowed_hash = int(link.is_hash_allowed(self._hashes))

src/pip/_internal/metadata/importlib/_envs.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88
import zipimport
99
from typing import Iterator, List, Optional, Sequence, Set, Tuple
1010

11-
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
11+
from pip._vendor.packaging.utils import (
12+
InvalidWheelFilename,
13+
NormalizedName,
14+
canonicalize_name,
15+
parse_wheel_filename,
16+
)
1217

1318
from pip._internal.metadata.base import BaseDistribution, BaseEnvironment
14-
from pip._internal.models.wheel import Wheel
1519
from pip._internal.utils.deprecation import deprecated
1620
from pip._internal.utils.filetypes import WHEEL_EXTENSION
1721

@@ -26,7 +30,9 @@ def _looks_like_wheel(location: str) -> bool:
2630
return False
2731
if not os.path.isfile(location):
2832
return False
29-
if not Wheel.wheel_file_re.match(os.path.basename(location)):
33+
try:
34+
parse_wheel_filename(os.path.basename(location))
35+
except InvalidWheelFilename:
3036
return False
3137
return zipfile.is_zipfile(location)
3238

src/pip/_internal/models/wheel.py

+64-43
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
"""
44

55
import re
6-
from typing import Dict, Iterable, List
6+
from typing import Dict, Iterable, List, Optional
77

88
from pip._vendor.packaging.tags import Tag
9+
from pip._vendor.packaging.utils import BuildTag, parse_wheel_filename
910
from pip._vendor.packaging.utils import (
10-
InvalidWheelFilename as PackagingInvalidWheelName,
11+
InvalidWheelFilename as _PackagingInvalidWheelFilename,
1112
)
12-
from pip._vendor.packaging.utils import parse_wheel_filename
1313

1414
from pip._internal.exceptions import InvalidWheelFilename
1515
from pip._internal.utils.deprecation import deprecated
@@ -18,54 +18,75 @@
1818
class Wheel:
1919
"""A wheel file"""
2020

21-
wheel_file_re = re.compile(
21+
legacy_wheel_file_re = re.compile(
2222
r"""^(?P<namever>(?P<name>[^\s-]+?)-(?P<ver>[^\s-]*?))
2323
((-(?P<build>\d[^-]*?))?-(?P<pyver>[^\s-]+?)-(?P<abi>[^\s-]+?)-(?P<plat>[^\s-]+?)
2424
\.whl|\.dist-info)$""",
2525
re.VERBOSE,
2626
)
2727

2828
def __init__(self, filename: str) -> None:
29-
"""
30-
:raises InvalidWheelFilename: when the filename is invalid for a wheel
31-
"""
32-
wheel_info = self.wheel_file_re.match(filename)
33-
if not wheel_info:
34-
raise InvalidWheelFilename(f"{filename} is not a valid wheel filename.")
3529
self.filename = filename
36-
self.name = wheel_info.group("name").replace("_", "-")
37-
_version = wheel_info.group("ver")
38-
if "_" in _version:
39-
try:
40-
parse_wheel_filename(filename)
41-
except PackagingInvalidWheelName as e:
42-
deprecated(
43-
reason=(
44-
f"Wheel filename {filename!r} is not correctly normalised. "
45-
"Future versions of pip will raise the following error:\n"
46-
f"{e.args[0]}\n\n"
47-
),
48-
replacement=(
49-
"to rename the wheel to use a correctly normalised "
50-
"name (this may require updating the version in "
51-
"the project metadata)"
52-
),
53-
gone_in="25.1",
54-
issue=12938,
55-
)
56-
57-
_version = _version.replace("_", "-")
58-
59-
self.version = _version
60-
self.build_tag = wheel_info.group("build")
61-
self.pyversions = wheel_info.group("pyver").split(".")
62-
self.abis = wheel_info.group("abi").split(".")
63-
self.plats = wheel_info.group("plat").split(".")
64-
65-
# All the tag combinations from this file
66-
self.file_tags = {
67-
Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats
68-
}
30+
31+
# To make mypy happy specify type hints that can come from either
32+
# parse_wheel_filename or the legacy_wheel_file_re match.
33+
self.name: str
34+
self._build_tag: Optional[BuildTag] = None
35+
36+
try:
37+
wheel_info = parse_wheel_filename(filename)
38+
self.name, _version, self._build_tag, self.file_tags = wheel_info
39+
self.version = str(_version)
40+
except _PackagingInvalidWheelFilename as e:
41+
# Check if the wheel filename is in the legacy format
42+
legacy_wheel_info = self.legacy_wheel_file_re.match(filename)
43+
if not legacy_wheel_info:
44+
raise InvalidWheelFilename(e.args[0]) from None
45+
46+
deprecated(
47+
reason=(
48+
f"Wheel filename {filename!r} is not correctly normalised. "
49+
"Future versions of pip will raise the following error:\n"
50+
f"{e.args[0]}\n\n"
51+
),
52+
replacement=(
53+
"to rename the wheel to use a correctly normalised "
54+
"name (this may require updating the version in "
55+
"the project metadata)"
56+
),
57+
gone_in="25.3",
58+
issue=12938,
59+
)
60+
61+
self.name = legacy_wheel_info.group("name").replace("_", "-")
62+
self.version = legacy_wheel_info.group("ver").replace("_", "-")
63+
64+
# Generate the file tags from the legacy wheel filename
65+
pyversions = legacy_wheel_info.group("pyver").split(".")
66+
abis = legacy_wheel_info.group("abi").split(".")
67+
plats = legacy_wheel_info.group("plat").split(".")
68+
self.file_tags = frozenset(
69+
Tag(interpreter=py, abi=abi, platform=plat)
70+
for py in pyversions
71+
for abi in abis
72+
for plat in plats
73+
)
74+
75+
@property
76+
def build_tag(self) -> BuildTag:
77+
if self._build_tag is not None:
78+
return self._build_tag
79+
80+
# Parse the build tag from the legacy wheel filename
81+
legacy_wheel_info = self.legacy_wheel_file_re.match(self.filename)
82+
assert legacy_wheel_info is not None, "guaranteed by filename validation"
83+
build_tag = legacy_wheel_info.group("build")
84+
match = re.match(r"^(\d+)(.*)$", build_tag)
85+
assert match is not None, "guaranteed by filename validation"
86+
build_tag_groups = match.groups()
87+
self._build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
88+
89+
return self._build_tag
6990

7091
def get_formatted_file_tags(self) -> List[str]:
7192
"""Return the wheel's tags as a sorted list of strings."""

tests/functional/test_install_wheel.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def test_install_from_wheel_with_headers(script: PipTestEnvironment) -> None:
190190
dist_info_folder = script.site_packages / "headers.dist-0.1.dist-info"
191191
result.did_create(dist_info_folder)
192192

193-
header_scheme_path = get_header_scheme_path_for_script(script, "headers.dist")
193+
header_scheme_path = get_header_scheme_path_for_script(script, "headers-dist")
194194
header_path = header_scheme_path / "header.h"
195195
assert header_path.read_text() == header_text
196196

tests/unit/test_models_wheel.py

+19-10
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,43 @@ def test_std_wheel_pattern(self) -> None:
1212
w = Wheel("simple-1.1.1-py2-none-any.whl")
1313
assert w.name == "simple"
1414
assert w.version == "1.1.1"
15-
assert w.pyversions == ["py2"]
16-
assert w.abis == ["none"]
17-
assert w.plats == ["any"]
15+
assert w.build_tag == ()
16+
assert w.file_tags == frozenset(
17+
[Tag(interpreter="py2", abi="none", platform="any")]
18+
)
1819

1920
def test_wheel_pattern_multi_values(self) -> None:
2021
w = Wheel("simple-1.1-py2.py3-abi1.abi2-any.whl")
2122
assert w.name == "simple"
2223
assert w.version == "1.1"
23-
assert w.pyversions == ["py2", "py3"]
24-
assert w.abis == ["abi1", "abi2"]
25-
assert w.plats == ["any"]
24+
assert w.build_tag == ()
25+
assert w.file_tags == frozenset(
26+
[
27+
Tag(interpreter="py2", abi="abi1", platform="any"),
28+
Tag(interpreter="py2", abi="abi2", platform="any"),
29+
Tag(interpreter="py3", abi="abi1", platform="any"),
30+
Tag(interpreter="py3", abi="abi2", platform="any"),
31+
]
32+
)
2633

2734
def test_wheel_with_build_tag(self) -> None:
2835
# pip doesn't do anything with build tags, but theoretically, we might
2936
# see one, in this case the build tag = '4'
3037
w = Wheel("simple-1.1-4-py2-none-any.whl")
3138
assert w.name == "simple"
3239
assert w.version == "1.1"
33-
assert w.pyversions == ["py2"]
34-
assert w.abis == ["none"]
35-
assert w.plats == ["any"]
40+
assert w.build_tag == (4, "")
41+
assert w.file_tags == frozenset(
42+
[Tag(interpreter="py2", abi="none", platform="any")]
43+
)
3644

3745
def test_single_digit_version(self) -> None:
3846
w = Wheel("simple-1-py2-none-any.whl")
3947
assert w.version == "1"
4048

4149
def test_non_pep440_version(self) -> None:
42-
w = Wheel("simple-_invalid_-py2-none-any.whl")
50+
with pytest.warns(deprecation.PipDeprecationWarning):
51+
w = Wheel("simple-_invalid_-py2-none-any.whl")
4352
assert w.version == "-invalid-"
4453

4554
def test_missing_version_raises(self) -> None:

0 commit comments

Comments
 (0)