Skip to content

Commit

Permalink
feat: allow not set env criteria and compatibility check for env spec
Browse files Browse the repository at this point in the history
Signed-off-by: Frost Ming <[email protected]>
  • Loading branch information
frostming committed Jul 5, 2024
1 parent 78dfc38 commit 33613c9
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 19 deletions.
87 changes: 68 additions & 19 deletions src/dep_logic/tags/tags.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from __future__ import annotations

import sys
from dataclasses import dataclass
from platform import python_implementation, python_version
from enum import IntEnum, auto
from platform import python_implementation
from typing import TYPE_CHECKING

from ..specifiers import InvalidSpecifier, VersionSpecifier, parse_version_specifier
Expand Down Expand Up @@ -84,46 +86,61 @@ def parse(cls, name: str, gil_disabled: bool = False) -> Self:
)


class EnvCompatibility(IntEnum):
INCOMPATIBLE = auto()
LOWER_OR_EQUAL = auto()
HIGHER = auto()


@dataclass(frozen=True)
class EnvSpec:
requires_python: VersionSpecifier
platform: Platform
implementation: Implementation
platform: Platform | None = None
implementation: Implementation | None = None

def as_dict(self) -> dict[str, str | bool]:
return {
"requires_python": str(self.requires_python),
"platform": str(self.platform),
"implementation": self.implementation.name,
"gil_disabled": self.implementation.gil_disabled,
}
result: dict[str, str | bool] = {"requires_python": str(self.requires_python)}
if self.platform is not None:
result["platform"] = str(self.platform)
if self.implementation is not None:
result["implementation"] = self.implementation.name
result["gil_disabled"] = self.implementation.gil_disabled
return result

@classmethod
def from_spec(
cls,
requires_python: str,
platform: str,
implementation: str = "cpython",
platform: str | None = None,
implementation: str | None = None,
gil_disabled: bool = False,
) -> Self:
return cls(
_ensure_version_specifier(requires_python),
Platform.parse(platform),
Implementation.parse(implementation, gil_disabled=gil_disabled),
Platform.parse(platform) if platform else None,
Implementation.parse(implementation, gil_disabled=gil_disabled)
if implementation
else None,
)

@classmethod
def current(cls) -> Self:
requires_python = _ensure_version_specifier(f"=={python_version()}")
# XXX: Strip pre-release and post-release tags
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
requires_python = _ensure_version_specifier(f"=={python_version}")
platform = Platform.current()
implementation = Implementation.current()
return cls(requires_python, platform, implementation)

def _evaluate_python(
self, python_tag: str, abi_tag: str
) -> tuple[int, int, int] | None:
"""Return a tuple of (major, minor, abi) if the wheel is compatible with the environment, or None otherwise."""
impl, major, minor = python_tag[:2], python_tag[2], python_tag[3:]
if impl not in [self.implementation.short, "py"]:
if self.implementation is not None and impl not in [
self.implementation.short,
"py",
]:
return None
abi_impl = (
abi_tag.split("_", 1)[0]
Expand Down Expand Up @@ -152,6 +169,8 @@ def _evaluate_python(
return (int(major), int(minor or 0), 0 if abi_impl == "none" else 2)

def _evaluate_platform(self, platform_tag: str) -> int | None:
if self.platform is None:
return -1
platform_tags = [*self.platform.compatible_tags, "any"]
if platform_tag not in platform_tags:
return None
Expand Down Expand Up @@ -195,7 +214,37 @@ def wheel_compatibility(
)

def markers(self) -> dict[str, str]:
return {
"implementation_name": self.implementation.name,
**self.platform.markers(),
}
result = {}
if self.platform is not None:
result.update(self.platform.markers())
if self.implementation is not None:
result["implementation_name"] = self.implementation.name
return result

def compare(self, target: EnvSpec) -> EnvCompatibility:
if self == target:
return EnvCompatibility.LOWER_OR_EQUAL
if (self.requires_python & target.requires_python).is_empty():
return EnvCompatibility.INCOMPATIBLE
if (
self.implementation is not None
and target.implementation is not None
and self.implementation != target.implementation
):
return EnvCompatibility.INCOMPATIBLE
if self.platform is None or target.platform is None:
return EnvCompatibility.LOWER_OR_EQUAL
if self.platform.arch != target.platform.arch:
return EnvCompatibility.INCOMPATIBLE
if type(self.platform.os) is not type(target.platform.os):
return EnvCompatibility.INCOMPATIBLE

if hasattr(self.platform.os, "major") and hasattr(self.platform.os, "minor"):
if (self.platform.os.major, self.platform.os.minor) <= ( # type: ignore[attr-defined]
target.platform.os.major, # type: ignore[attr-defined]
target.platform.os.minor, # type: ignore[attr-defined]
):
return EnvCompatibility.LOWER_OR_EQUAL
else:
return EnvCompatibility.HIGHER
return EnvCompatibility.LOWER_OR_EQUAL
81 changes: 81 additions & 0 deletions tests/tags/test_tags.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import pytest

from dep_logic.tags import EnvSpec
from dep_logic.tags.tags import EnvCompatibility


def test_check_wheel_tags():
Expand Down Expand Up @@ -53,3 +56,81 @@ def test_check_wheel_tags():
"protobuf-5.27.2-cp38-abi3-macosx_10_9_universal2.whl",
"protobuf-5.27.2-py3-none-any.whl",
]

python_env = EnvSpec.from_spec(">=3.9")
wheel_compats = {
f: c
for f, c in {f: python_env.wheel_compatibility(f) for f in wheels}.items()
if c is not None
}
filtered_wheels = sorted(wheel_compats, key=wheel_compats.__getitem__, reverse=True)
assert filtered_wheels == [
"protobuf-5.27.2-cp310-cp310-macosx_12_0_arm64.whl",
"protobuf-5.27.2-cp310-abi3-win32.whl",
"protobuf-5.27.2-cp310-abi3-win_amd64.whl",
"protobuf-5.27.2-cp39-cp39-win32.whl",
"protobuf-5.27.2-cp39-cp39-win_amd64.whl",
"protobuf-5.27.2-cp38-abi3-macosx_10_9_universal2.whl",
"protobuf-5.27.2-cp38-abi3-manylinux2014_aarch64.whl",
"protobuf-5.27.2-cp38-abi3-manylinux2014_x86_64.whl",
"protobuf-5.27.2-py3-none-any.whl",
]


@pytest.mark.parametrize(
"left,right,expected",
[
(
EnvSpec.from_spec(">=3.9", "macos", "cpython"),
EnvSpec.from_spec(">=3.9", "macos", "cpython"),
EnvCompatibility.LOWER_OR_EQUAL,
),
(
EnvSpec.from_spec(">=3.9", "macos", "cpython"),
EnvSpec.from_spec(">=3.9", "macos"),
EnvCompatibility.LOWER_OR_EQUAL,
),
(
EnvSpec.from_spec(">=3.9", "macos"),
EnvSpec.from_spec(">=3.9", "macos", "cpython"),
EnvCompatibility.LOWER_OR_EQUAL,
),
(
EnvSpec.from_spec(">=3.9", "macos", "cpython"),
EnvSpec.from_spec(">=3.7,<3.10"),
EnvCompatibility.LOWER_OR_EQUAL,
),
(
EnvSpec.from_spec(">=3.7,<3.10"),
EnvSpec.from_spec(">=3.9", "macos", "cpython"),
EnvCompatibility.LOWER_OR_EQUAL,
),
(
EnvSpec.from_spec(">=3.9", "macos", "cpython"),
EnvSpec.from_spec("<3.8", "macos", "cpython"),
EnvCompatibility.INCOMPATIBLE,
),
(
EnvSpec.from_spec(">=3.9", "macos", "cpython"),
EnvSpec.from_spec("<3.10", "linux", "cpython"),
EnvCompatibility.INCOMPATIBLE,
),
(
EnvSpec.from_spec(">=3.9", "macos", "cpython"),
EnvSpec.from_spec("<3.10", "macos", "pypy"),
EnvCompatibility.INCOMPATIBLE,
),
(
EnvSpec.from_spec(">=3.9", "macos_x86_64", "cpython"),
EnvSpec.from_spec("<3.10", "macos_10_9_x86_64", "cpython"),
EnvCompatibility.HIGHER,
),
(
EnvSpec.from_spec("<3.10", "macos_10_9_x86_64", "cpython"),
EnvSpec.from_spec(">=3.9", "macos_x86_64", "cpython"),
EnvCompatibility.LOWER_OR_EQUAL,
),
],
)
def test_env_spec_comparison(left, right, expected):
assert left.compare(right) == expected

0 comments on commit 33613c9

Please sign in to comment.