Skip to content

Commit 76fc136

Browse files
authored
Merge pull request #13581 from notatallshaw/remove-legacy-wheel-filename-support
Remove legacy wheel filename support
2 parents 81b3c05 + f241616 commit 76fc136

File tree

4 files changed

+128
-78
lines changed

4 files changed

+128
-78
lines changed

news/13581.removal.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Remove support for non-standard legacy wheel filenames.

src/pip/_internal/models/wheel.py

Lines changed: 5 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,91 +4,30 @@
44

55
from __future__ import annotations
66

7-
import re
87
from collections.abc import Iterable
98

109
from pip._vendor.packaging.tags import Tag
11-
from pip._vendor.packaging.utils import BuildTag, parse_wheel_filename
1210
from pip._vendor.packaging.utils import (
1311
InvalidWheelFilename as _PackagingInvalidWheelFilename,
1412
)
13+
from pip._vendor.packaging.utils import parse_wheel_filename
1514

1615
from pip._internal.exceptions import InvalidWheelFilename
17-
from pip._internal.utils.deprecation import deprecated
1816

1917

2018
class Wheel:
2119
"""A wheel file"""
2220

23-
legacy_wheel_file_re = re.compile(
24-
r"""^(?P<namever>(?P<name>[^\s-]+?)-(?P<ver>[^\s-]*?))
25-
((-(?P<build>\d[^-]*?))?-(?P<pyver>[^\s-]+?)-(?P<abi>[^\s-]+?)-(?P<plat>[^\s-]+?)
26-
\.whl|\.dist-info)$""",
27-
re.VERBOSE,
28-
)
29-
3021
def __init__(self, filename: str) -> None:
3122
self.filename = filename
3223

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

9332
def get_formatted_file_tags(self) -> list[str]:
9433
"""Return the wheel's tags as a sorted list of strings."""
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""Test that pip index versions handles invalid (non-PEP 440) wheel filenames.
2+
3+
This test was added for robustness after legacy wheel filename support
4+
was removed in pip 25.3.
5+
"""
6+
7+
import json
8+
import textwrap
9+
from pathlib import Path
10+
11+
from tests.lib import PipTestEnvironment
12+
from tests.lib.wheel import make_wheel
13+
14+
15+
def _create_test_index_with_invalid_wheels(
16+
tmpdir: Path, package_name: str = "pkg"
17+
) -> Path:
18+
"""Create a test index with both valid and invalid wheel filenames.
19+
20+
Returns the path to the index directory.
21+
"""
22+
# Create test index
23+
index_dir = tmpdir / "test_index"
24+
index_dir.mkdir()
25+
26+
(index_dir / "index.html").write_text(
27+
textwrap.dedent(
28+
f"""\
29+
<!DOCTYPE html>
30+
<html>
31+
<body><a href="{package_name}/index.html">{package_name}</a></body>
32+
</html>
33+
"""
34+
)
35+
)
36+
37+
pkg_dir = index_dir / package_name
38+
pkg_dir.mkdir()
39+
40+
valid_wheels = [
41+
(f"{package_name}-1.0.0-py3-none-any.whl", "1.0.0"),
42+
(f"{package_name}-2.0.0-py3-none-any.whl", "2.0.0"),
43+
]
44+
invalid_wheels = [
45+
(f"{package_name}-3.0_1-py3-none-any.whl", "3.0"), # underscore in version
46+
(f"{package_name}-_bad_-py3-none-any.wh", "0.0.0"), # no version
47+
(
48+
f"{package_name}-5.0.0_build1-py3-none-any.whl",
49+
"5.0.0",
50+
), # underscore in build tag
51+
]
52+
53+
all_wheels = valid_wheels + invalid_wheels
54+
for wheel_name, version in all_wheels:
55+
wheel = make_wheel(name=package_name, version=version)
56+
wheel.save_to(pkg_dir / wheel_name)
57+
58+
# Create package index
59+
links = [
60+
f'<a href="{wheel_name}">{wheel_name}</a><br/>' for wheel_name, _ in all_wheels
61+
]
62+
(pkg_dir / "index.html").write_text(
63+
textwrap.dedent(
64+
f"""\
65+
<!DOCTYPE html>
66+
<html>
67+
<body>
68+
{''.join(links)}
69+
</body>
70+
</html>
71+
"""
72+
)
73+
)
74+
75+
return index_dir
76+
77+
78+
def test_index_versions_ignores_invalid_wheel_names(
79+
script: PipTestEnvironment,
80+
tmpdir: Path,
81+
) -> None:
82+
"""Test that pip index versions ignores invalid wheel names."""
83+
index_dir = _create_test_index_with_invalid_wheels(tmpdir)
84+
85+
# Run pip index versions with JSON output
86+
result = script.pip(
87+
"index", "versions", "pkg", "--json", "--index-url", index_dir.as_uri()
88+
)
89+
90+
assert result.returncode == 0
91+
92+
output = json.loads(result.stdout)
93+
assert output["name"] == "pkg"
94+
assert output["latest"] == "2.0.0"
95+
96+
expected_versions = ["2.0.0", "1.0.0"]
97+
assert output["versions"] == expected_versions
98+
99+
100+
def test_install_ignores_invalid_wheel_names(
101+
script: PipTestEnvironment,
102+
tmpdir: Path,
103+
) -> None:
104+
"""Test that pip install ignores invalid wheel names and installs valid ones."""
105+
index_dir = _create_test_index_with_invalid_wheels(tmpdir)
106+
107+
# Run pip install - should ignore invalid wheels and install the latest valid one
108+
result = script.pip(
109+
"install", "pkg", "--no-cache-dir", "--index-url", index_dir.as_uri()
110+
)
111+
112+
assert result.returncode == 0
113+
script.assert_installed(pkg="2.0.0")

tests/unit/test_models_wheel.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from pip._internal.exceptions import InvalidWheelFilename
66
from pip._internal.models.wheel import Wheel
7-
from pip._internal.utils import compatibility_tags, deprecation
7+
from pip._internal.utils import compatibility_tags
88

99

1010
class TestWheelFile:
@@ -47,9 +47,8 @@ def test_single_digit_version(self) -> None:
4747
assert w.version == "1"
4848

4949
def test_non_pep440_version(self) -> None:
50-
with pytest.warns(deprecation.PipDeprecationWarning):
51-
w = Wheel("simple-_invalid_-py2-none-any.whl")
52-
assert w.version == "-invalid-"
50+
with pytest.raises(InvalidWheelFilename):
51+
Wheel("simple-_invalid_-py2-none-any.whl")
5352

5453
def test_missing_version_raises(self) -> None:
5554
with pytest.raises(InvalidWheelFilename):
@@ -254,16 +253,14 @@ def test_support_index_min__none_supported(self) -> None:
254253

255254
def test_version_underscore_conversion(self) -> None:
256255
"""
257-
Test that we convert '_' to '-' for versions parsed out of wheel
258-
filenames
256+
Test that underscore versions are now invalid (no longer converted)
259257
"""
260-
with pytest.warns(deprecation.PipDeprecationWarning):
261-
w = Wheel("simple-0.1_1-py2-none-any.whl")
262-
assert w.version == "0.1-1"
258+
with pytest.raises(InvalidWheelFilename):
259+
Wheel("simple-0.1_1-py2-none-any.whl")
263260

264-
def test_invalid_wheel_warning(self) -> None:
261+
def test_invalid_wheel_raises(self) -> None:
265262
"""
266-
Test that wheel with invalid name produces warning
263+
Test that wheel with invalid name now raises exception
267264
"""
268-
with pytest.warns(deprecation.PipDeprecationWarning):
265+
with pytest.raises(InvalidWheelFilename):
269266
Wheel("six-1.16.0_build1-py3-none-any.whl")

0 commit comments

Comments
 (0)