Skip to content

Commit

Permalink
Merge pull request #96 from koterpillar/architecture-preference
Browse files Browse the repository at this point in the history
Filter architectures first
  • Loading branch information
koterpillar committed Dec 27, 2023
2 parents 932d23d + aa8f97f commit 0e49055
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 43 deletions.
30 changes: 23 additions & 7 deletions mybox/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,46 @@

from .utils import T, allow_singular_none

Filter = Callable[[T], bool]


class Filters(BaseModel):
@staticmethod
def includes_(substring: str) -> Callable[[str], bool]:
def includes_(substring: str) -> Filter[str]:
return lambda x: substring in x.lower()

@staticmethod
def excludes_(substring: str) -> Callable[[str], bool]:
def excludes_(substring: str) -> Filter[str]:
return lambda x: substring not in x

@staticmethod
def startswith(prefix: str) -> Callable[[str], bool]:
def startswith(prefix: str) -> Filter[str]:
return lambda x: x.startswith(prefix)

@staticmethod
def endswith(suffix: str) -> Callable[[str], bool]:
def endswith(suffix: str) -> Filter[str]:
return lambda x: x.endswith(suffix)

@staticmethod
def regex_(regex: str) -> Callable[[str], bool]:
def regex_(regex: str) -> Filter[str]:
regex_compiled = re.compile(regex)

return lambda x: regex_compiled.match(x) is not None

@classmethod
def from_synonyms(
cls, items: dict[str, list[str]], key: str
) -> Iterator[Filter[str]]:
yield cls.includes_(key)
for synonym in items.get(key, []):
yield cls.includes_(synonym)

for other, synonyms in items.items():
if other == key:
continue
for synonym in [other, *synonyms]:
yield cls.excludes_(synonym)

prefixes: list[str] = Field(default_factory=list, alias="prefix")
prefixes_val = allow_singular_none("prefixes")

Expand All @@ -44,7 +60,7 @@ def regex_(regex: str) -> Callable[[str], bool]:
regex: list[str] = Field(default_factory=list)
regex_val = allow_singular_none("regex")

def filters(self) -> Iterator[Callable[[str], bool]]:
def filters(self) -> Iterator[Filter[str]]:
for prefix in self.prefixes:
yield self.startswith(prefix)
for suffix in self.suffixes:
Expand All @@ -57,7 +73,7 @@ def filters(self) -> Iterator[Callable[[str], bool]]:
yield self.regex_(regex)


def choose(candidates: list[T], filters: Iterator[Callable[[T], bool]]) -> T:
def choose(candidates: list[T], filters: Iterator[Filter[T]]) -> T:
if len(candidates) == 0:
raise ValueError("No candidates to choose from.")
while len(candidates) > 1:
Expand Down
51 changes: 26 additions & 25 deletions mybox/package/github.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import os
from dataclasses import dataclass
from subprocess import CalledProcessError
from typing import Any, Callable, Iterator, Optional
from typing import Any, Iterator, Optional

import requests

from ..driver import OS, Architecture
from ..filters import Filters, choose
from ..filters import Filter, Filters, choose
from ..utils import async_cached, async_cached_lock, run_output
from .archive import ArchivePackage

Expand Down Expand Up @@ -53,11 +53,18 @@ class GitHubRelease:


ARCHITECTURE_FILTERS: dict[str, list[str]] = {
"x86_64": ["amd64", "x64"],
"arm64": ["aarch64", "arm"],
"i386": ["i686", "x86"],
"powerpc64": ["ppc64"],
"mips": [],
"powerpc": ["ppc"],
"s390x": [],
"x86_64": ["amd64", "x64"],
}

OS_FILTERS: dict[str, list[str]] = {
"darwin": ["macos", "osx"],
"linux": [],
"windows": [],
}


Expand All @@ -81,32 +88,26 @@ async def latest_release(self) -> GitHubRelease:
@classmethod
def environment_filters(
cls, *, target_os: OS, target_arch: Architecture
) -> Iterator[Callable[[str], bool]]:
for hint in [".tar.gz"]:
yield cls.includes_(hint)
) -> Iterator[Filter[str]]:
for signature_hint in [".asc", ".sig", "sha256", "sha512", ".yml"]:
yield cls.excludes_(signature_hint)

for other_os_hint in [".exe", ".dmg"]:
yield cls.excludes_(other_os_hint)
for os_hint in target_os.switch(
linux=[
cls.includes_("linux"),
cls.includes_("gnu"),
cls.excludes_("musl"),
],
macos=[cls.includes_(hint) for hint in ["macos", "darwin", "osx"]],
):
yield os_hint
for system_package_hint in [".deb", ".rpm", ".dmg", ".exe"]:
yield cls.excludes_(system_package_hint)

yield from cls.from_synonyms(
OS_FILTERS, target_os.switch(linux="linux", macos="darwin")
)

yield from cls.from_synonyms(ARCHITECTURE_FILTERS, target_arch)

for arch, synonyms in ARCHITECTURE_FILTERS.items():
method = cls.includes_ if arch == target_arch else cls.excludes_
for synonym in [arch, *synonyms]:
yield method(synonym)
if target_os.switch(linux=True, macos=False):
yield cls.includes_("gnu")
yield cls.excludes_("musl")

def all_filters(
self, *, target_os: OS, target_arch: Architecture
) -> Iterator[Callable[[str], bool]]:
) -> Iterator[Filter[str]]:
yield from self.filters()
yield from self.environment_filters(
target_os=target_os, target_arch=target_arch
Expand All @@ -116,8 +117,8 @@ async def artifact(self) -> GitHubReleaseArtifact:
candidates = (await self.latest_release()).assets

def candidate_filter(
name_filter: Callable[[str], bool]
) -> Callable[[GitHubReleaseArtifact], bool]:
name_filter: Filter,
) -> Filter[GitHubReleaseArtifact]:
return lambda candidate: name_filter(candidate.name)

target_os = await self.driver.os()
Expand Down
24 changes: 14 additions & 10 deletions tests/package/test_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,22 +153,26 @@ async def check_installed(self):
]


class TestGitHubCLI(PackageTestBase):
async def constructor_args(self) -> PackageArgs:
return {"repo": "cli/cli", "binary": "gh", "strip": 1}

async def check_installed_command(self):
return ["gh", "--version"]

check_installed_output = "gh version"


class TestJQ(PackageTestBase):
async def constructor_args(self) -> PackageArgs:
return {
"repo": "stedolan/jq",
"include": "linux64",
"binary": "jq-linux64",
"raw": True,
"repo": "jqlang/jq",
"binary": "jq",
"raw": "jq",
"raw_executable": True,
}

async def check_installed_command(self):
return ["jq-linux64", "--version"]
return ["jq", "--version"]

check_installed_output = "jq-"

async def check_applicable(self) -> None:
await super().check_applicable()
if not (await self.driver.os()).switch(linux=True, macos=False):
pytest.skip("This test is only applicable on Linux.")
7 changes: 6 additions & 1 deletion tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

import pytest

from mybox.filters import choose
from mybox.filters import Filters, choose


def test_filters_attributes():
filters = Filters(prefix=["a", "b"], suffix="c", include="2", exclude="3")
assert choose(["a1c", "a2c", "a23c", "b1d", "a1e"], filters.filters()) == "a2c"


class TestChoose:
Expand Down

0 comments on commit 0e49055

Please sign in to comment.