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 13, 2024
1 parent aa8fc2a commit e161e4b
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 75 deletions.
76 changes: 38 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,25 @@ 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):
monkeypatch.setattr(wheel, "wheel_file_re", pretend.stub(match=lambda a: None))
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="Invalid wheel filename"):
wheel.Wheel(file_name)


def test_read_valid(example_wheel):
Expand All @@ -72,32 +70,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)


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)


def test_read_wheel_empty_metadata(tmpdir):
def test_read_wheel_missing_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)

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)


def test_read_wheel_empty_metadata(example_wheel, monkeypatch):
"""Raise an exception when a wheel file is missing METADATA."""
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)
57 changes: 20 additions & 37 deletions twine/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
import os
import re
import zipfile
from typing import List, Optional
from contextlib import suppress
from typing import Optional
from typing import cast as type_cast

from pkginfo import distribution
Expand All @@ -42,55 +43,37 @@
class Wheel(distribution.Distribution):
def __init__(self, filename: str, metadata_version: Optional[str] = None) -> None:
self.filename = filename
self.basefilename = os.path.basename(self.filename)
self.metadata_version = metadata_version
self.extractMetadata()

@property
def py_version(self) -> str:
wheel_info = wheel_file_re.match(self.basefilename)
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)]
# The class constructor raises an exception if the filename does
# not match this regular expression, thus the match is guaranteed
# to succeed.
m = wheel_file_re.match(os.path.basename(self.filename))
assert m is not None, "for mypy"
return m.group("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)
if not os.path.exists(self.filename):
raise exceptions.InvalidDistribution(f"No such file: {self.filename}")

else:
m = wheel_file_re.match(os.path.basename(self.filename))
if not m:
raise exceptions.InvalidDistribution(
"Not a known archive format for file: %s" % fqn
f"Invalid wheel filename: {self.filename}"
)

searched_files: List[str] = []
try:
for path in self.find_candidate_metadata_files(names):
candidate = "/".join(path)
data = read_file(candidate)
name, version = m.group("name", "version")
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"{name}-{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 METADATA "
f"missing 'Metadata-Version': {self.filename}"
)

def parse(self, data: bytes) -> None:
Expand Down

0 comments on commit e161e4b

Please sign in to comment.