Skip to content
Merged
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
46 changes: 43 additions & 3 deletions src/runtime/dlplugins/datalab/plugins/wheels.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from packaging.markers import default_environment
from packaging.requirements import InvalidRequirement, Requirement
from packaging.specifiers import InvalidSpecifier, SpecifierSet
from packaging.tags import Tag, compatible_tags, interpreter_name
from packaging.utils import canonicalize_name, parse_wheel_filename
from packaging.version import InvalidVersion, Version

Expand Down Expand Up @@ -72,6 +73,40 @@ class WheelInspectionError(ValueError):
"""Raised when an archive cannot be accepted as a plugin wheel."""


def _host_python(python_version: str | None) -> Version:
return Version(
python_version or f"{sys.version_info.major}.{sys.version_info.minor}"
)


def _pure_python_tags(python: Version) -> frozenset[Tag]:
"""Return the pure-Python wheel tags a Python version installs, as pip does."""
interpreter = f"{interpreter_name()}{python.major}{python.minor}"
return frozenset(
compatible_tags((python.major, python.minor), interpreter, ["any"])
)


def is_compatible_wheel(
filename: str, requires_python: str | None, python_version: str | None = None
) -> bool:
"""Return True if a Python version can install a pure-Python wheel.

Args:
filename: Wheel file name, which carries its tags
requires_python: ``Requires-Python`` metadata of the wheel, if any
python_version: Host Python version (default: running interpreter)
"""
try:
wheel_tags = parse_wheel_filename(filename)[3]
python = _host_python(python_version)
if requires_python and python not in SpecifierSet(requires_python):
return False
except (InvalidSpecifier, InvalidVersion, ValueError):
return False
return bool(wheel_tags & _pure_python_tags(python))


def _is_stdlib_module(name: str) -> bool:
stdlib_names = getattr(sys, "stdlib_module_names", None)
if stdlib_names is not None:
Expand Down Expand Up @@ -310,6 +345,13 @@ def inspect_wheel(
raise WheelInspectionError(f"Invalid wheel filename: {filename!r}") from exc
if any(tag.abi != "none" or tag.platform != "any" for tag in wheel_tags):
raise WheelInspectionError("Only pure-Python *-none-any wheels are supported")
current_python = _host_python(python_version)
if not wheel_tags & _pure_python_tags(current_python):
interpreters = ".".join(sorted({tag.interpreter for tag in wheel_tags}))
raise WheelInspectionError(
f"Plugin wheel is built for {interpreters}, "
f"host has Python {current_python}"
)

try:
archive = zipfile.ZipFile(io.BytesIO(data))
Expand Down Expand Up @@ -350,9 +392,6 @@ def inspect_wheel(
raise WheelInspectionError("Wheel filename and METADATA version differ")

required_python = package_metadata.get("Requires-Python")
current_python = Version(
python_version or f"{sys.version_info.major}.{sys.version_info.minor}"
)
if required_python:
try:
specifier = SpecifierSet(required_python)
Expand Down Expand Up @@ -401,4 +440,5 @@ def inspect_wheel(
"WEB_ENTRY_POINT_GROUP",
"WheelInspectionError",
"inspect_wheel",
"is_compatible_wheel",
]
20 changes: 20 additions & 0 deletions tests/python/test_wheel_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,26 @@ def test_inspection_rejects_unsafe_payloads(
inspect(make_wheel(top_level=top_level, extra_files=extra_files))


def test_inspection_rejects_wheel_built_for_another_python() -> None:
"""Pyodide's Python must be able to install the wheel's interpreter tag."""
filename = FILENAME.replace("py3-none-any", "cp313-none-any")
data = make_wheel()

with pytest.raises(WheelInspectionError, match="built for cp313"):
inspect_wheel(
data,
filename=filename,
available_distributions={"sigima": "1.2.0"},
python_version="3.12",
)
assert inspect_wheel(
data,
filename=filename,
available_distributions={"sigima": "1.2.0"},
python_version="3.13",
)["tags"] == ["cp313-none-any"]


def test_inspection_rejects_missing_host_dependency() -> None:
with pytest.raises(WheelInspectionError, match="not provided by DataLab-Web"):
inspect_wheel(
Expand Down
Loading