Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions build/tests/test_wheel_smoke_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Unit tests for wheel_smoke_check.py's interpreter choice.

No mocks - each test writes a real .whl (a zip carrying a real dist-info
METADATA) and the code under test reads it exactly as it reads a wheel
maturin produced. The install and the smoke run itself need a real
interpreter and a real compiled extension, so they are exercised by the
CI job that runs this script against the built dist/, not here.
"""

import zipfile
from pathlib import Path

import pytest

from wheel_smoke_check import _python_version_for, _requires_python_floor, _tag_version


def _wheel(tmp_path: Path, name: str, requires_python: str | None) -> Path:
"""A real wheel file: the filename tag plus a dist-info METADATA."""
path = tmp_path / name
metadata = "Metadata-Version: 2.1\nName: vector-vrl\nVersion: 1.0.8\n"
if requires_python is not None:
metadata += f"Requires-Python: {requires_python}\n"
with zipfile.ZipFile(path, "w") as archive:
archive.writestr("vector_vrl-1.0.8.dist-info/METADATA", metadata)
return path


class TestTagVersion:
"""The CPython version the filename tag names."""

def test_reads_the_tag(self, tmp_path):
wheel = _wheel(tmp_path, "vector_vrl-1.0.8-cp312-abi3-linux_x86_64.whl", None)
assert _tag_version(wheel) == (3, 12)

def test_reads_a_two_digit_minor(self, tmp_path):
wheel = _wheel(tmp_path, "vector_vrl-1.0.8-cp314-abi3-linux_x86_64.whl", None)
assert _tag_version(wheel) == (3, 14)

def test_refuses_a_tag_it_cannot_parse(self, tmp_path):
wheel = _wheel(tmp_path, "vector_vrl-1.0.8-py3-none-any.whl", None)
with pytest.raises(ValueError, match="can't parse a CPython version"):
_tag_version(wheel)


class TestRequiresPythonFloor:
"""The lower bound the wheel's own metadata declares."""

def test_reads_the_floor(self, tmp_path):
wheel = _wheel(tmp_path, "vector_vrl-1.0.8-cp312-abi3-linux_x86_64.whl", ">=3.14")
assert _requires_python_floor(wheel) == (3, 14)

def test_tolerates_whitespace(self, tmp_path):
wheel = _wheel(tmp_path, "vector_vrl-1.0.8-cp312-abi3-linux_x86_64.whl", ">= 3.14")
assert _requires_python_floor(wheel) == (3, 14)

def test_none_when_undeclared(self, tmp_path):
wheel = _wheel(tmp_path, "vector_vrl-1.0.8-cp312-abi3-linux_x86_64.whl", None)
assert _requires_python_floor(wheel) is None

def test_none_when_the_bound_has_no_lower_half(self, tmp_path):
wheel = _wheel(tmp_path, "vector_vrl-1.0.8-cp312-abi3-linux_x86_64.whl", "<4.0")
assert _requires_python_floor(wheel) is None


class TestPythonVersionFor:
"""The venv must satisfy both bounds, and the abi3 tag is the lesser one."""

def test_the_declared_floor_wins_over_an_abi3_tag(self, tmp_path):
# The 1.0.8 release: abi3-py312 wheel, >=3.14 metadata. Building the
# venv on the tag gave a 3.12 interpreter the wheel then refused.
wheel = _wheel(tmp_path, "vector_vrl-1.0.8-cp312-abi3-linux_x86_64.whl", ">=3.14")
assert _python_version_for(wheel) == "3.14"

def test_the_tag_wins_when_it_is_the_higher_bound(self, tmp_path):
wheel = _wheel(tmp_path, "vector_vrl-1.0.8-cp314-abi3-linux_x86_64.whl", ">=3.12")
assert _python_version_for(wheel) == "3.14"

def test_the_tag_stands_alone_when_nothing_is_declared(self, tmp_path):
wheel = _wheel(tmp_path, "vector_vrl-1.0.8-cp312-abi3-linux_x86_64.whl", None)
assert _python_version_for(wheel) == "3.12"

def test_equal_bounds_agree(self, tmp_path):
wheel = _wheel(tmp_path, "vector_vrl-1.0.8-cp314-abi3-linux_x86_64.whl", ">=3.14")
assert _python_version_for(wheel) == "3.14"
49 changes: 39 additions & 10 deletions build/wheel_smoke_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,56 @@
import subprocess
import sys
import tempfile
import zipfile
from email.parser import BytesParser
from pathlib import Path

_PYTHON_TAG_RE = re.compile(r"^cp(\d)(\d+)$")
_REQUIRES_FLOOR_RE = re.compile(r">=\s*(\d+)\.(\d+)")


def _python_version_for(wheel: Path) -> str:
"""The CPython version (e.g. "3.12") a wheel's own filename tag names.

A wheel is tied to the exact interpreter it was built against unless
tagged abi3 - installing it into a mismatched venv fails outright, and
the CI runner's ambient default Python is not guaranteed to match
whatever maturin-action picked, so this must be read from the wheel
itself rather than assumed.
"""
def _tag_version(wheel: Path) -> tuple[int, int]:
"""The CPython version a wheel's own filename tag names, as (major, minor)."""
python_tag = wheel.stem.split("-")[2]
match = _PYTHON_TAG_RE.match(python_tag)
if not match:
raise ValueError(
f"can't parse a CPython version from wheel tag {python_tag!r} ({wheel.name})"
)
return f"{match.group(1)}.{match.group(2)}"
return int(match.group(1)), int(match.group(2))


def _requires_python_floor(wheel: Path) -> tuple[int, int] | None:
"""The lower bound of the wheel's own ``Requires-Python``, if it declares one."""
with zipfile.ZipFile(wheel) as archive:
names = [n for n in archive.namelist() if n.endswith(".dist-info/METADATA")]
if not names:
return None
with archive.open(names[0]) as handle:
metadata = BytesParser().parse(handle, headersonly=True)
declared = metadata.get("Requires-Python")
if not declared:
return None
match = _REQUIRES_FLOOR_RE.search(declared)
return (int(match.group(1)), int(match.group(2))) if match else None


def _python_version_for(wheel: Path) -> str:
"""The CPython version (e.g. "3.14") to build the throwaway venv on.

Two constraints, and the venv must satisfy both. The filename tag names
the interpreter a non-abi3 wheel is tied to, and the CI runner's ambient
default is not guaranteed to match whatever maturin-action picked. An
abi3 wheel is forward compatible, so there its tag is a FLOOR rather than
an exact requirement, and the package's own ``Requires-Python`` can sit
above it -- an abi3-py312 wheel declaring >=3.14 refuses to install on
3.12, which is the wheel telling the truth and the tag being the lesser
of the two bounds.
"""
tag = _tag_version(wheel)
floor = _requires_python_floor(wheel)
major, minor = max(tag, floor) if floor else tag
return f"{major}.{minor}"


# Mirrors README.md's worked example and the Vector class golden path.
Expand Down
Loading