Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Fixed

- micropip now respects the `yanked` flag in the PyPI simple API.
Comment thread
ryanking13 marked this conversation as resolved.
Outdated
[#208](https://github.com/pyodide/micropip/pull/208)

## [0.9.0] - 2024/02/01

### Fixed
Expand Down
7 changes: 7 additions & 0 deletions micropip/package_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ def _compatible_wheels(
# Size of the file in bytes, if available (PEP 700)
# This key is not available in the Simple API HTML response, so this field may be None
size = file.get("size")

# PEP-592:
# yanked can be an arbitrary string (reason) or bool.
# any string is considered as True, so we convert it to bool.
yanked = bool(file.get("yanked", False))

Comment thread
ryanking13 marked this conversation as resolved.
yield WheelInfo.from_package_index(
name=name,
filename=filename,
Expand All @@ -171,6 +177,7 @@ def _compatible_wheels(
sha256=sha256,
size=size,
core_metadata=core_metadata,
yanked=yanked,
)

@classmethod
Expand Down
41 changes: 32 additions & 9 deletions micropip/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import importlib.metadata
import logging
import warnings
from collections.abc import Iterable
from dataclasses import dataclass, field
from importlib.metadata import PackageNotFoundError
from urllib.parse import urlparse
Expand Down Expand Up @@ -327,6 +328,8 @@ def find_wheel(metadata: ProjectInfo, req: Requirement) -> WheelInfo:
reverse=True,
)

yanked_versions: list[list[WheelInfo]] = []
Comment thread
agriyakhetarpal marked this conversation as resolved.

for ver in candidate_versions:
if ver not in releases:
warnings.warn(
Expand All @@ -335,22 +338,42 @@ def find_wheel(metadata: ProjectInfo, req: Requirement) -> WheelInfo:
)
continue

best_wheel = None
best_tag_index = float("infinity")
wheels = list(releases[ver])

# If the version is yanked, put it in the end of the candidate list.
# If we can't find a wheel that satisfies the requirement,
# install the yanked version as a last resort.
yanked = any(wheel.yanked for wheel in wheels)
if yanked:
yanked_versions.append(wheels)
continue
Comment thread
agriyakhetarpal marked this conversation as resolved.

wheels = releases[ver]
for wheel in wheels:
tag_index = best_compatible_tag_index(wheel.tags)
if tag_index is not None and tag_index < best_tag_index:
best_wheel = wheel
best_tag_index = tag_index
best_wheel = _find_best_wheel(wheels)

if best_wheel is not None:
return wheel
return best_wheel

for wheels in yanked_versions:
best_wheel = _find_best_wheel(wheels)

if best_wheel is not None:
return best_wheel

raise ValueError(
f"Can't find a pure Python 3 wheel for '{req}'.\n"
f"See: {FAQ_URLS['cant_find_wheel']}\n"
"You can use `await micropip.install(..., keep_going=True)` "
"to get a list of all packages with missing wheels."
)


def _find_best_wheel(wheels: Iterable[WheelInfo]) -> WheelInfo | None:
best_wheel = None
best_tag_index = float("infinity")
for wheel in wheels:
tag_index = best_compatible_tag_index(wheel.tags)
if tag_index is not None and tag_index < best_tag_index:
best_wheel = wheel
best_tag_index = tag_index

return best_wheel
3 changes: 3 additions & 0 deletions micropip/wheelinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class WheelInfo:
sha256: str | None = None
size: int | None = None # Size in bytes, if available (PEP 700)
core_metadata: DistributionMetadata = None # Wheel's metadata (PEP 658 / PEP-714)
yanked: bool = False # Whether the wheel has been yanked (PEP-592)

# Fields below are only available after downloading the wheel, i.e. after calling `download()`.

Expand Down Expand Up @@ -100,6 +101,7 @@ def from_package_index(
sha256: str | None,
size: int | None,
core_metadata: DistributionMetadata = None,
yanked: bool = False,
) -> "WheelInfo":
"""Extract available metadata from response received from package index"""
parsed_url = urlparse(url)
Expand All @@ -116,6 +118,7 @@ def from_package_index(
sha256=sha256,
size=size,
core_metadata=core_metadata,
yanked=yanked,
)

async def install(self, target: Path) -> None:
Expand Down
36 changes: 36 additions & 0 deletions tests/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,42 @@ def test_find_wheel_invalid_version():
assert str(wheel.version) == "0.15.5"


def test_yanked_version():
from micropip._vendored.packaging.src.packaging.requirements import Requirement
from micropip.transaction import find_wheel

versions = ["0.0.1", "0.15.5", "0.9.1"]
Comment thread
agriyakhetarpal marked this conversation as resolved.

# Mark 0.15.5 as yanked
# convert generator --> list and monkeypatch the yanked value
metadata = _pypi_metadata("dummy_module", {v: ["py3"] for v in versions})
for version in list(metadata.releases):
wheels = list(metadata.releases[version])
for wheel in wheels:
if str(wheel.version) == "0.15.5":
wheel.yanked = True

metadata.releases[version] = wheels

# yanked version should be skipped and the next best version should be selected
requirement1 = Requirement("dummy_module")
wheel = find_wheel(metadata, requirement1)

assert str(wheel.version) == "0.9.1"

requirement2 = Requirement("dummy_module==0.15.5")
wheel = find_wheel(metadata, requirement2)

# no other compatible version available, so the yanked version should be selected
assert str(wheel.version) == "0.15.5"

requirement3 = Requirement("dummy_module>0.10.0")

wheel = find_wheel(metadata, requirement3)

assert str(wheel.version) == "0.15.5"


_best_tag_test_cases = (
"package, version, incompatible_tags, compatible_tags",
# Tests assume that `compatible_tags` is sorted from least to most compatible:
Expand Down