Skip to content

Commit d5a1763

Browse files
fix(wheelfile): resolve .dist-info path case-insensitively when reading wheels (#686)
A wheel filename may contain non-lowercase characters (e.g. Django-3.2.5.whl) while the .dist-info directory inside uses normalized lowercase naming (django-3.2.5.dist-info/). WheelFile previously derived dist_info_path strictly from the filename, causing a 'Missing RECORD file' error on open. Resolve the actual .dist-info/RECORD path case-insensitively from the zip namelist when the expected path is not found. Fixes #411. --------- Co-authored-by: Alex Grönholm <alex.gronholm@nextday.fi>
1 parent 5718957 commit d5a1763

3 files changed

Lines changed: 42 additions & 0 deletions

File tree

docs/news.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ Release Notes
33

44
**UNRELEASED**
55

6+
- Fixed ``WheelFile`` raising ``Missing RECORD file`` when the wheel filename
7+
contains uppercase characters (e.g. ``Django-3.2.5.whl``) but the
8+
``.dist-info`` directory inside uses normalized lowercase naming
9+
(`#411 <https://github.com/pypa/wheel/issues/411>`_)
610
- Added the ``wheel info`` subcommand to display metadata about wheel files without
711
unpacking them (`#639 <https://github.com/pypa/wheel/issues/639>`_)
812

src/wheel/wheelfile.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,17 @@ def __init__(
8282
self._file_hashes: dict[str, tuple[None, None] | tuple[int, bytes]] = {}
8383
self._file_sizes = {}
8484
if mode == "r":
85+
# The .dist-info directory inside the wheel may use normalized
86+
# (lowercase) naming even when the filename does not. Resolve the
87+
# actual path case-insensitively.
88+
if self.record_path not in self.namelist():
89+
lowered = self.dist_info_path.lower() + "/record"
90+
for name in self.namelist():
91+
if name.lower() == lowered:
92+
self.dist_info_path = name.rsplit("/RECORD", 1)[0]
93+
self.record_path = name
94+
break
95+
8596
# Ignore RECORD and any embedded wheel signatures
8697
self._file_hashes[self.record_path] = None, None
8798
self._file_hashes[self.record_path + ".jws"] = None, None

tests/test_wheelfile.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,33 @@ def test_missing_record(wheel_path: Path) -> None:
5454
exc.match("^Missing test-1.0.dist-info/RECORD file$")
5555

5656

57+
def test_mixed_case_dist_info(tmp_path: Path) -> None:
58+
"""Regression test: wheel filename has uppercase but .dist-info dir is lowercase.
59+
60+
A wheel named ``Django-3.2.5.whl`` may contain ``django-3.2.5.dist-info/``
61+
inside (normalized). WheelFile should find RECORD case-insensitively.
62+
See `#411 <https://github.com/pypa/wheel/issues/411>`_.
63+
"""
64+
wheel_path = tmp_path / "MixedCase-1.0-py3-none-any.whl"
65+
with ZipFile(wheel_path, "w", ZIP_DEFLATED) as zf:
66+
zf.writestr("mixedcase/__init__.py", "")
67+
# Use lowercase dist-info (as pip/build tools produce)
68+
zf.writestr("mixedcase-1.0.dist-info/WHEEL", "Wheel-Version: 1.0\n")
69+
zf.writestr(
70+
"mixedcase-1.0.dist-info/METADATA",
71+
"Metadata-Version: 2.1\nName: MixedCase\nVersion: 1.0\n",
72+
)
73+
zf.writestr(
74+
"mixedcase-1.0.dist-info/RECORD",
75+
"mixedcase/__init__.py,,\n",
76+
)
77+
78+
# Should not raise — this is the fix
79+
with WheelFile(wheel_path) as wf:
80+
assert wf.dist_info_path == "mixedcase-1.0.dist-info"
81+
assert wf.record_path == "mixedcase-1.0.dist-info/RECORD"
82+
83+
5784
def test_unsupported_hash_algorithm(wheel_path: Path) -> None:
5885
with ZipFile(wheel_path, "w") as zf:
5986
zf.writestr("hello/héllö.py", 'print("Héllö, w0rld!")\n')

0 commit comments

Comments
 (0)