Skip to content

Commit

Permalink
Tighten and simplify lookup for METADATA member in wheel archives
Browse files Browse the repository at this point in the history
The location of the METADATA member in the wheel archive is well
defined. It is not necessary nor desirable look for it elsewhere.

See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#file-contents

This simplifies the implementation and is more correct.

Modernize the code a bit, while at it.
  • Loading branch information
dnicolodi committed Dec 17, 2024
1 parent 9ff643a commit 6eeb3ab
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 77 deletions.
75 changes: 37 additions & 38 deletions tests/test_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
# limitations under the License.
import os
import pathlib
import re
import zipfile

import pretend
Expand All @@ -40,26 +39,24 @@ def test_version_parsing(example_wheel):
assert example_wheel.py_version == "py2.py3"


def test_version_parsing_missing_pyver(monkeypatch, example_wheel):
wheel.wheel_file_re = pretend.stub(match=lambda a: None)
assert example_wheel.py_version == "any"
@pytest.mark.parametrize(
"file_name,valid",
[
("name-1.2.3-py313-none-any.whl", True),
("name-1.2.3-42-py313-none-any.whl", True),
("long_name-1.2.3-py3-none-any.whl", True),
("missing_components-1.2.3.whl", False),
],
)
def test_parse_wheel_file_name(file_name, valid):
assert bool(wheel.wheel_file_re.match(file_name)) == valid


def test_find_metadata_files():
names = [
"package/lib/__init__.py",
"package/lib/version.py",
"package/METADATA.txt",
"package/METADATA.json",
"package/METADATA",
]
expected = [
["package", "METADATA"],
["package", "METADATA.json"],
["package", "METADATA.txt"],
]
candidates = wheel.Wheel.find_candidate_metadata_files(names)
assert expected == candidates
def test_invalid_file_name(monkeypatch):
parent = pathlib.Path(__file__).parent
file_name = str(parent / "fixtures" / "twine-1.5.0-py2.whl")
with pytest.raises(exceptions.InvalidDistribution, match="Invalid wheel filename"):
wheel.Wheel(file_name)


def test_read_valid(example_wheel):
Expand All @@ -72,32 +69,34 @@ def test_read_valid(example_wheel):
def test_read_non_existent_wheel_file_name():
"""Raise an exception when wheel file doesn't exist."""
file_name = str(pathlib.Path("/foo/bar/baz.whl").resolve())
with pytest.raises(
exceptions.InvalidDistribution, match=re.escape(f"No such file: {file_name}")
):
with pytest.raises(exceptions.InvalidDistribution, match="No such file"):
wheel.Wheel(file_name).read()


def test_read_invalid_wheel_extension():
"""Raise an exception when file is missing .whl extension."""
file_name = str(pathlib.Path(__file__).parent / "fixtures" / "twine-1.5.0.tar.gz")
with pytest.raises(
exceptions.InvalidDistribution,
match=re.escape(f"Not a known archive format for file: {file_name}"),
):
with pytest.raises(exceptions.InvalidDistribution, match="Invalid wheel filename"):
wheel.Wheel(file_name).read()


def test_read_wheel_missing_metadata(example_wheel, monkeypatch):
"""Raise an exception when a wheel file is missing METADATA."""

def patch(self, name):
raise KeyError

monkeypatch.setattr(zipfile.ZipFile, "read", patch)
parent = pathlib.Path(__file__).parent
file_name = str(parent / "fixtures" / "twine-1.5.0-py2.py3-none-any.whl")
with pytest.raises(exceptions.InvalidDistribution, match="No METADATA in archive"):
wheel.Wheel(file_name).read()


def test_read_wheel_empty_metadata(tmpdir):
def test_read_wheel_empty_metadata(example_wheel, monkeypatch):
"""Raise an exception when a wheel file is missing METADATA."""
whl_file = tmpdir.mkdir("wheel").join("not-a-wheel.whl")
with zipfile.ZipFile(whl_file, "w") as zip_file:
zip_file.writestr("METADATA", "")

with pytest.raises(
exceptions.InvalidDistribution,
match=re.escape(
f"No METADATA in archive or METADATA missing 'Metadata-Version': {whl_file}"
),
):
wheel.Wheel(whl_file).read()
monkeypatch.setattr(zipfile.ZipFile, "read", lambda self, name: b"")
parent = pathlib.Path(__file__).parent
file_name = str(parent / "fixtures" / "twine-1.5.0-py2.py3-none-any.whl")
with pytest.raises(exceptions.InvalidDistribution, match="No METADATA in archive"):
wheel.Wheel(file_name).read()
54 changes: 15 additions & 39 deletions twine/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import os
import re
import zipfile
from typing import List
from contextlib import suppress

from twine import distribution
from twine import exceptions
Expand All @@ -33,53 +33,29 @@

class Wheel(distribution.Distribution):
def __init__(self, filename: str) -> None:
m = wheel_file_re.match(os.path.basename(filename))
if not m:
raise exceptions.InvalidDistribution(f"Invalid wheel filename: {filename}")
self.name = m.group("name")
self.version = m.group("version")
self.pyver = m.group("pyver")

if not os.path.exists(filename):
raise exceptions.InvalidDistribution(f"No such file: {filename}")
self.filename = filename

@property
def py_version(self) -> str:
wheel_info = wheel_file_re.match(os.path.basename(self.filename))
if wheel_info is None:
return "any"
else:
return wheel_info.group("pyver")

@staticmethod
def find_candidate_metadata_files(names: List[str]) -> List[List[str]]:
"""Filter files that may be METADATA files."""
tuples = [x.split("/") for x in names if "METADATA" in x]
return [x[1] for x in sorted((len(x), x) for x in tuples)]
return self.pyver

def read(self) -> bytes:
fqn = os.path.abspath(os.path.normpath(self.filename))
if not os.path.exists(fqn):
raise exceptions.InvalidDistribution("No such file: %s" % fqn)

if fqn.endswith(".whl"):
archive = zipfile.ZipFile(fqn)
names = archive.namelist()

def read_file(name: str) -> bytes:
return archive.read(name)

else:
raise exceptions.InvalidDistribution(
"Not a known archive format for file: %s" % fqn
)

searched_files: List[str] = []
try:
for path in self.find_candidate_metadata_files(names):
candidate = "/".join(path)
data = read_file(candidate)
with zipfile.ZipFile(self.filename) as wheel:
with suppress(KeyError):
# The wheel needs to contain the METADATA file at this location.
data = wheel.read(f"{self.name}-{self.version}.dist-info/METADATA")
if b"Metadata-Version" in data:
return data
searched_files.append(candidate)
finally:
archive.close()

raise exceptions.InvalidDistribution(
"No METADATA in archive or METADATA missing 'Metadata-Version': "
"%s (searched %s)" % (fqn, ",".join(searched_files))
"No METADATA in archive or "
f"METADATA missing 'Metadata-Version': {self.filename}"
)

0 comments on commit 6eeb3ab

Please sign in to comment.