Compare commits

..

1 Commits

Author SHA1 Message Date
1365fc48bf Include default system and arch synonyms
All checks were successful
continuous-integration/drone/push Build is passing
Some projects use different system and arch names in their assets.
Sometimes due to convention or differeing tools and systems. For
example, on macOS 13.6, Python will return the system as `Darwin`.
However, some release assets will be named `macOS` or `macos`. Similarly
`arm64` and `aarch64` are used interchangeably.

This patch adds a few lists of synonymous values such that
release-gitter can make an attempt at matching the intended binary.
These lists of synonyms can be expanded to be more complete as time goes
on.

These synonyms are only used if there is no user provided mapping. In
the case that any user provided mapping exists, the map will be the
sole source of truth. Eg. If you provide a map for `Windows=>windows`,
no other values will be mapped and we won't assume that `Darwin=>macos`
anymore.
2023-11-01 15:52:34 -07:00
8 changed files with 98 additions and 525 deletions

View File

@ -6,7 +6,6 @@ PYTHON_VERSIONS = [
"3.9",
"3.10",
"3.11",
"3.12",
"latest",
]
@ -72,9 +71,6 @@ def test_step(docker_tag, python_cmd="python"):
"{} -V".format(python_cmd),
"make clean-all test"
],
"environment": {
"PIP_CACHE_DIR": ".pip-cache",
},
}

View File

@ -1,11 +1,11 @@
---
repos:
- repo: https://github.com/psf/black
rev: 24.4.2
rev: 22.3.0
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v4.1.0
hooks:
- id: check-added-large-files
- id: check-merge-conflict
@ -14,12 +14,12 @@ repos:
- id: trailing-whitespace
- id: name-tests-test
exclude: tests/(common.py|util.py|(helpers|integration/factories)/(.+).py)
- repo: https://github.com/pycqa/isort
rev: 5.13.2
- repo: https://github.com/asottile/reorder_python_imports
rev: v3.0.1
hooks:
- id: isort
- id: reorder-python-imports
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
rev: v0.942
hooks:
- id: mypy
exclude: docs/

View File

@ -13,8 +13,6 @@
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
from __future__ import annotations
project = "release-gitter"
copyright = "2021, iamthefij"
author = "iamthefij"

View File

@ -2,13 +2,13 @@
This builder functions as a pseudo builder that instead downloads and installs a binary file using
release-gitter based on a pyproject.toml file. It's a total hack...
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from shutil import copy
from shutil import copytree
from shutil import move
import toml
from wheel.wheelfile import WheelFile
@ -17,9 +17,11 @@ import release_gitter as rg
from release_gitter import removeprefix
PACKAGE_NAME = "pseudo"
@dataclass
class Config:
name: str
format: str
git_url: str
hostname: str
@ -37,52 +39,56 @@ class Config:
include_extra_files: list[str] | None = None
def download(config: Config, wheel_scripts: Path) -> list[Path]:
"""Download and extract files to the wheel_scripts directory"""
return rg.download_release(
rg.GitRemoteInfo(config.hostname, config.owner, config.repo),
wheel_scripts,
def download(config: Config) -> list[Path]:
release = rg.fetch_release(
rg.GitRemoteInfo(config.hostname, config.owner, config.repo), config.version
)
asset = rg.match_asset(
release,
config.format,
version=config.version,
system_mapping=config.map_system,
arch_mapping=config.map_arch,
extract_files=config.extract_files,
pre_release=config.pre_release,
exec=config.exec,
)
files = rg.download_asset(asset, extract_files=config.extract_files)
# Optionally execute post command
if config.exec:
rg.check_call(config.exec, shell=True)
return files
def read_metadata() -> Config:
"""Read configuration from pyproject.toml"""
pyproject = toml.load("pyproject.toml").get("tool", {}).get("release-gitter")
if not pyproject:
config = toml.load("pyproject.toml").get("tool", {}).get("release-gitter")
if not config:
raise ValueError("Must have configuration in [tool.release-gitter]")
git_url = pyproject.pop("git-url", None)
remote_info = rg.parse_git_url(git_url)
git_url = config.pop("git-url", None)
remote_info = rg.parse_git_remote(git_url)
config = Config(
name=pyproject.pop("name", remote_info.repo),
format=pyproject.pop("format"),
args = Config(
format=config.pop("format"),
git_url=git_url,
hostname=pyproject.pop("hostname", remote_info.hostname),
owner=pyproject.pop("owner", remote_info.owner),
repo=pyproject.pop("repo", remote_info.repo),
hostname=config.pop("hostname", remote_info.hostname),
owner=config.pop("owner", remote_info.owner),
repo=config.pop("repo", remote_info.repo),
)
for key, value in pyproject.items():
setattr(config, str(key).replace("-", "_"), value)
for key, value in config.items():
setattr(args, str(key).replace("-", "_"), value)
if config.version is None:
config.version = rg.read_version(
config.version_git_tag,
not config.version_git_no_fetch,
if args.version is None:
args.version = rg.read_version(
args.version_git_tag,
not args.version_git_no_fetch,
)
if config.extract_all:
config.extract_files = []
if args.extract_all:
args.extract_files = []
return config
return args
class _PseudoBuildBackend:
@ -99,7 +105,7 @@ class _PseudoBuildBackend:
version = removeprefix(metadata.version, "v") if metadata.version else "0.0.0"
# Returns distinfo dir?
dist_info = Path(metadata_directory) / f"{metadata.name}-{version}.dist-info"
dist_info = Path(metadata_directory) / f"{PACKAGE_NAME}-{version}.dist-info"
dist_info.mkdir()
# Write metadata
@ -108,7 +114,7 @@ class _PseudoBuildBackend:
"\n".join(
[
"Metadata-Version: 2.1",
f"Name: {metadata.name}",
f"Name: {PACKAGE_NAME}",
f"Version: {version}",
]
)
@ -140,8 +146,6 @@ class _PseudoBuildBackend:
def build_wheel(
self, wheel_directory, config_settings=None, metadata_directory=None
):
if metadata_directory is None:
raise ValueError("Cannot build wheel without metadata_directory")
metadata_directory = Path(metadata_directory)
metadata = read_metadata()
@ -150,13 +154,15 @@ class _PseudoBuildBackend:
wheel_directory = Path(wheel_directory)
wheel_directory.mkdir(exist_ok=True)
wheel_scripts = wheel_directory / f"{metadata.name}-{version}.data/scripts"
wheel_scripts = wheel_directory / f"{PACKAGE_NAME}-{version}.data/scripts"
wheel_scripts.mkdir(parents=True, exist_ok=True)
copytree(metadata_directory, wheel_directory / metadata_directory.name)
metadata = read_metadata()
download(metadata, wheel_scripts)
files = download(metadata)
for file in files:
move(file, wheel_scripts / file.name)
for file_name in metadata.include_extra_files or []:
file = Path(file_name)
@ -169,11 +175,11 @@ class _PseudoBuildBackend:
print(f"ls {wheel_directory}: {list(wheel_directory.rglob('*'))}")
wheel_filename = f"{metadata.name}-{version}-py2.py3-none-any.whl"
wheel_filename = f"{PACKAGE_NAME}-{version}-py2.py3-none-any.whl"
with WheelFile(wheel_directory / wheel_filename, "w") as wf:
print("Repacking wheel as {}...".format(wheel_filename), end="")
# sys.stdout.flush()
wf.write_files(str(wheel_directory))
wf.write_files(wheel_directory)
return wheel_filename

View File

@ -1,46 +0,0 @@
from __future__ import annotations
import shutil
import subprocess
import venv
from pathlib import Path
from unittest import TestCase
ITEST_VENV_PATH = Path("venv-itest")
class TestPseudoBuilder(TestCase):
def setUp(self):
venv.create(
ITEST_VENV_PATH,
system_site_packages=False,
clear=True,
with_pip=True,
)
self.pip_install("-e", ".[builder]")
def tearDown(self):
shutil.rmtree(ITEST_VENV_PATH)
def pip_install(self, *args: str):
subprocess.run(
[str(ITEST_VENV_PATH.joinpath("bin", "pip")), "install", *args],
check=True,
)
def test_install_remote_package(self):
self.assertTrue(ITEST_VENV_PATH.exists())
self.assertTrue(ITEST_VENV_PATH.joinpath("bin", "python").exists())
self.assertTrue(ITEST_VENV_PATH.joinpath("bin", "pip").exists())
itest_packages = {
"stylua": "git+https://github.com/JohnnyMorganz/StyLua",
"selene": "git+https://github.com/amitds1997/selene",
}
for package, source in itest_packages.items():
self.pip_install("--no-index", "--no-build-isolation", source)
# Check if the package is installed
assert ITEST_VENV_PATH.joinpath("bin", package).exists()
# Check if the package has executable permissions
assert ITEST_VENV_PATH.joinpath("bin", package).stat().st_mode & 0o111

View File

@ -48,12 +48,12 @@ dependencies = [
[tool.hatch.envs.test.scripts]
run = [
"coverage erase",
"coverage run --source=release_gitter,pseudo_builder -m unittest discover -p '*_test.py'",
"coverage run --source=release_gitter -m unittest discover . *_test.py",
"coverage report -m # --fail-under 70",
]
[[tool.hatch.envs.test.matrix]]
python = ["3", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
python = ["3", "3.7", "3.8", "3.9", "3.10", "3.11"]
[tool.hatch.envs.lint]
detached = true
@ -62,8 +62,3 @@ dependencies = ["pre-commit"]
[tool.hatch.envs.lint.scripts]
all = "pre-commit run --all-files"
install-hooks = "pre-commit install --install-hooks"
[tool.isort]
add_imports = ["from __future__ import annotations"]
force_single_line = true
profile = "black"

View File

@ -2,9 +2,7 @@
from __future__ import annotations
import argparse
import logging
import platform
import tempfile
from collections.abc import Sequence
from dataclasses import dataclass
from io import BytesIO
@ -16,16 +14,12 @@ from subprocess import check_output
from tarfile import TarFile
from tarfile import TarInfo
from typing import Any
from typing import NamedTuple
from urllib.parse import urlparse
from zipfile import ZipFile
import requests
__version__ = "3.0.3"
logging.basicConfig(level=logging.WARNING)
__version__ = "2.2.1"
class UnsupportedContentTypeError(ValueError):
@ -55,16 +49,16 @@ def removesuffix(s: str, suf: str) -> str:
SYSTEM_SYNONYMS: list[list[str]] = [
["Darwin", "darwin", "MacOS", "macos", "macOS"],
["Windows", "windows", "win", "win32", "win64"],
["Darwin", "darwin", "macos", "macOS"],
["Windows", "windows", "win", "win64"],
["Linux", "linux"],
]
ARCH_SYNONYMS: list[list[str]] = [
["arm"],
["x86_64", "amd64", "AMD64"],
["x86_64", "amd64"],
["arm64", "aarch64", "armv8b", "armv8l"],
["x86", "i386", "i686"],
["i386", "x86"],
]
@ -79,12 +73,6 @@ def get_synonyms(value: str, thesaurus: list[list[str]]) -> list[str]:
return results
class MatchedValues(NamedTuple):
version: str
system: str
arch: str
@dataclass
class GitRemoteInfo:
"""Extracts information about a repository"""
@ -126,13 +114,13 @@ class GitRemoteInfo:
)
def read_git_remote() -> str:
"""Reads the git remote url from the origin"""
return check_output(["git", "remote", "get-url", "origin"]).decode("UTF-8").strip()
def parse_git_url(git_url: str) -> GitRemoteInfo:
def parse_git_remote(git_url: str | None = None) -> GitRemoteInfo:
"""Extract Github repo info from a git remote url"""
if not git_url:
git_url = (
check_output(["git", "remote", "get-url", "origin"]).decode("UTF-8").strip()
)
# Normalize Github ssh url as a proper URL
if git_url.startswith("git@github.com:"):
git_ssh_parts = git_url.partition(":")
@ -179,7 +167,6 @@ def read_git_tag(fetch: bool = True) -> str | None:
def read_version(from_tags: bool = False, fetch: bool = False) -> str | None:
"""Read version information from file or from git"""
if from_tags:
logging.debug("Reading version from git tag")
return read_git_tag(fetch)
matchers = {
@ -189,13 +176,10 @@ def read_version(from_tags: bool = False, fetch: bool = False) -> str | None:
for name, extractor in matchers.items():
p = Path(name)
if p.exists():
logging.debug(f"Reading version from {p}")
return extractor(p)
logging.warning(
"Unknown local project version. Didn't find any of %s", set(matchers.keys())
)
# TODO: Log this out to stderr
# raise ValueError(f"Unknown project type. Didn't find any of {matchers.keys()}")
return None
@ -218,8 +202,6 @@ def fetch_release(
# Return the latest if requested
if version is None or version == "latest":
logging.debug("Looking for latest release")
for release in result.json():
if release["prerelease"] and not pre_release:
continue
@ -229,8 +211,6 @@ def fetch_release(
# Return matching version
for release in result.json():
if release["tag_name"].endswith(version):
logging.debug(f"Found release {release['name']} matching version {version}")
return release
raise ValueError(
@ -245,7 +225,7 @@ def match_asset(
version: str | None = None,
system_mapping: dict[str, str] | None = None,
arch_mapping: dict[str, str] | None = None,
) -> tuple[dict[Any, Any], MatchedValues]:
) -> dict[Any, Any]:
"""Accepts a release and searches for an appropriate asset attached using
a provided template and some alternative mappings for version, system, and machine info
@ -281,7 +261,13 @@ def match_asset(
# This should never really happen
if version is None:
raise ValueError("No version provided or found in release name.")
if "{version}" in format:
raise ValueError(
"No version provided or found in release name but is in format"
)
else:
# This should never happen, but since version isn't used anywhere, we can make it an empty string
version = ""
system = platform.system()
if system_mapping:
@ -300,7 +286,7 @@ def match_asset(
version=version_opt,
system=system_opt,
arch=arch_opt,
): MatchedValues(version=version_opt, system=system_opt, arch=arch_opt)
)
for version_opt, system_opt, arch_opt in product(
(
version.lstrip("v"),
@ -313,7 +299,7 @@ def match_asset(
for asset in release["assets"]:
if asset["name"] in expected_names:
return (asset, expected_names[asset["name"]])
return asset
raise ValueError(
f"Could not find asset named {expected_names} on release {release['name']}"
@ -329,12 +315,8 @@ class PackageAdapter:
"application/zip",
"application/x-zip-compressed",
):
logging.debug("Opening zip file from response content")
self._package = ZipFile(BytesIO(response.content))
elif content_type == "application/x-tar":
logging.debug("Opening tar file from response content")
self._package = TarFile(fileobj=response.raw)
elif content_type in (
"application/gzip",
@ -342,8 +324,6 @@ class PackageAdapter:
"application/x-tar+xz",
"application/x-compressed-tar",
):
logging.debug("Opening compressed tar file from response content")
self._package = TarFile.open(fileobj=BytesIO(response.content), mode="r:*")
else:
raise UnsupportedContentTypeError(
@ -354,7 +334,6 @@ class PackageAdapter:
"""Get list of all file names in package"""
if isinstance(self._package, ZipFile):
return self._package.namelist()
if isinstance(self._package, TarFile):
return self._package.getnames()
@ -372,26 +351,19 @@ class PackageAdapter:
If the `file_names` list is empty, all files will be extracted"""
if path is None:
path = Path.cwd()
if not members:
logging.debug("Extracting all members to %s", path)
self._package.extractall(path=path)
return self.get_names()
# TODO: Use walrus operator when dropping 3.7 support
missing_members = set(members) - set(self.get_names())
if missing_members:
raise ValueError(f"Missing members: {missing_members}")
logging.debug("Extracting members %s to %s", members, path)
if isinstance(self._package, ZipFile):
self._package.extractall(path=path, members=members)
if isinstance(self._package, TarFile):
self._package.extractall(
path=path, members=(self._package.getmember(name) for name in members)
path=path, members=(TarInfo(name) for name in members)
)
return members
@ -414,7 +386,7 @@ def get_asset_package(
continue
else:
raise UnsupportedContentTypeError(
f"Cannot extract files from archive because we don't recognize the content types {possible_content_types}"
"Cannot extract files from archive because we don't recognize the content type"
)
@ -441,10 +413,8 @@ def download_asset(
result = requests.get(asset["browser_download_url"])
if extract_files is not None:
logging.info("Extracting package %s", asset["name"])
package = get_asset_package(asset, result)
extract_files = package.extractall(path=destination, members=extract_files)
return [destination / name for name in extract_files]
file_name = destination / asset["name"]
@ -491,7 +461,6 @@ class MapAddAction(argparse.Action):
def _parse_args(args: list[str] | None = None) -> argparse.Namespace:
logging.debug("Parsing arguments: %s", args)
parser = argparse.ArgumentParser()
parser.add_argument(
"format",
@ -505,9 +474,7 @@ def _parse_args(args: list[str] | None = None) -> argparse.Namespace:
default=Path.cwd(),
help="Destination directory. Defaults to current directory",
)
parser.add_argument(
"-v", action="count", help="verbose or debug logging", default=0
)
parser.add_argument("-v", action="store_true", help="verbose logging")
parser.add_argument(
"--hostname",
help="Git repository hostname",
@ -578,29 +545,12 @@ def _parse_args(args: list[str] | None = None) -> argparse.Namespace:
action="store_true",
help="Only print the URL and do not download",
)
parser.add_argument(
"--use-temp-dir",
action="store_true",
help="Use a temporary directory as the destination",
)
parsed_args = parser.parse_args(args)
# Merge in fields from args and git remote
if not all((parsed_args.owner, parsed_args.repo, parsed_args.hostname)):
# Check to see if a git url was provided. If not, we use local directory git remote
if parsed_args.git_url is None:
parsed_args.git_url = read_git_remote()
# If using a local repo, try to determine version from project files
if parsed_args.version is None:
parsed_args.version = read_version(
parsed_args.version_git_tag,
not parsed_args.version_git_no_fetch,
)
# Get parts from git url
remote_info = parse_git_url(parsed_args.git_url)
remote_info = parse_git_remote(parsed_args.git_url)
def merge_field(a, b, field):
value = getattr(a, field)
@ -610,12 +560,15 @@ def _parse_args(args: list[str] | None = None) -> argparse.Namespace:
for field in ("owner", "repo", "hostname"):
merge_field(parsed_args, remote_info, field)
if parsed_args.version is None:
parsed_args.version = read_version(
parsed_args.version_git_tag,
not parsed_args.version_git_no_fetch,
)
if parsed_args.extract_all:
parsed_args.extract_files = []
if parsed_args.use_temp_dir:
parsed_args.destination = Path(tempfile.mkdtemp())
return parsed_args
@ -628,99 +581,55 @@ def download_release(
arch_mapping: dict[str, str] | None = None,
extract_files: list[str] | None = None,
pre_release=False,
exec: str | None = None,
) -> list[Path]:
"""Convenience method for fetching, downloading, and extracting a release
This is slightly different than running off the commandline, it will execute the shell script
from the destination directory, not the current working directory.
"""
"""Convenience method for fetching, downloading and extracting a release"""
release = fetch_release(
remote_info,
version=version,
pre_release=pre_release,
)
asset, matched_values = match_asset(
asset = match_asset(
release,
format,
version=version,
system_mapping=system_mapping,
arch_mapping=arch_mapping,
)
format_fields = dict(
asset_name=asset["name"],
**matched_values._asdict(),
)
formatted_files = (
[file.format(**format_fields) for file in extract_files]
if extract_files is not None
else None
)
files = download_asset(
asset,
extract_files=formatted_files,
extract_files=extract_files,
destination=destination,
)
if exec:
check_call(
exec.format(asset["name"], **format_fields), shell=True, cwd=destination
)
return files
def main():
args = _parse_args()
logging.getLogger().setLevel(30 - 10 * args.v)
# Fetch the release
release = fetch_release(
GitRemoteInfo(args.hostname, args.owner, args.repo),
version=args.version,
pre_release=args.prerelease,
)
logging.debug("Found release: %s", release["name"])
version = args.version or release["tag_name"]
logging.debug("Release version: %s", version)
# Find the asset to download using mapping rules
asset, matched_values = match_asset(
asset = match_asset(
release,
args.format,
version=version,
version=args.version,
system_mapping=args.map_system,
arch_mapping=args.map_arch,
)
logging.info(f"Downloading {asset['name']} from release {release['name']}")
if args.v:
print(f"Downloading {asset['name']} from release {release['name']}")
if args.url_only:
print(asset["browser_download_url"])
return
format_fields = dict(
asset_name=asset["name"],
**matched_values._asdict(),
)
# Format files to extract with version info, as this is sometimes included
formatted_files = (
[file.format(**format_fields) for file in args.extract_files]
if args.extract_files is not None
else None
)
files = download_asset(
asset,
extract_files=formatted_files,
extract_files=args.extract_files,
destination=args.destination,
)
@ -728,11 +637,7 @@ def main():
# Optionally execute post command
if args.exec:
check_call(
args.exec.format(asset["name"], **format_fields),
shell=True,
cwd=args.destination,
)
check_call(args.exec.format(asset["name"]), shell=True)
if __name__ == "__main__":

View File

@ -1,12 +1,11 @@
from __future__ import annotations
import unittest
from itertools import chain
from itertools import product
from tarfile import TarFile
from typing import Any
from typing import Callable
from typing import NamedTuple
from typing import Optional
from unittest.mock import MagicMock
from unittest.mock import mock_open
from unittest.mock import patch
@ -22,11 +21,10 @@ class TestExpression(NamedTuple):
args: list[Any]
kwargs: dict[str, Any]
expected: Any
exception: type[Exception] | None = None
msg: str | None = None
exception: Optional[type[Exception]] = None
def run(self, f: Callable):
with self.t.subTest(msg=self.msg, f=f, args=self.args, kwargs=self.kwargs):
with self.t.subTest(f=f, args=self.args, kwargs=self.kwargs):
try:
result = f(*self.args, **self.kwargs)
self.t.assertIsNone(
@ -82,7 +80,7 @@ class TestRemoteInfo(unittest.TestCase):
release_gitter.InvalidRemoteError,
),
):
test_case.run(release_gitter.parse_git_url)
test_case.run(release_gitter.parse_git_remote)
def test_generate_release_url(self):
for subtest in (
@ -199,284 +197,5 @@ class TestContentTypeDetection(unittest.TestCase):
)
def first_result(f):
def wrapper(*args, **kwargs):
return f(*args, **kwargs)[0]
return wrapper
class TestMatchAsset(unittest.TestCase):
def test_match_asset_versions(self, *_):
# Input variations:
# Case 1: Version provided with prefix
# Case 2: Version provided without prefix
# Case 3: No version provided, tag exists in release
# These should be impossible
# Case 4: No version provided, tag doesn't exist in release but not in template
# Case 5: No version provided, tag doesn't exist in release and is in template
# Release variations:
# Case 1: tag_name with version prefix
# Case 2: tag_name without version prefix
# File variations:
# Case 1: file name with version prefix
# Case 2: file name without version prefix
def new_expression(version: str | None, tag_name: str, file_name: str):
release = {"tag_name": tag_name, "assets": [{"name": file_name}]}
expected = {"name": file_name}
return TestExpression(
self, [release, "file-{version}.zip", version], {}, expected
)
happy_cases = [
new_expression(version, tag_name, file_name)
for version, tag_name, file_name in product(
("v1.0.0", "1.0.0", None),
("v1.0.0", "1.0.0"),
("file-v1.0.0.zip", "file-1.0.0.zip"),
)
]
for test_case in happy_cases:
test_case.run(first_result(release_gitter.match_asset))
def test_match_asset_systems(self, *_):
# Input variations:
# Case 1: System mapping provided
# Case 2: No system mapping provided
# Test: We want to show that default matching will work out of the box with some values for the current machine
# Test: We want to show that non-standard mappings will always work if provided manually
def run_with_context(actual_system: str, *args, **kwargs):
with patch("platform.system", return_value=actual_system):
return release_gitter.match_asset(*args, **kwargs)
def new_expression(
actual_system: str,
system_mapping: dict[str, str] | None,
file_name: str,
expected: dict[str, str],
exception: type[Exception] | None = None,
msg: str | None = None,
):
release = {
"name": "v1.0.0",
"tag_name": "v1.0.0",
"assets": [{"name": file_name}],
}
return TestExpression(
self,
[actual_system, release, "file-{system}.zip"],
{"system_mapping": system_mapping},
expected,
exception,
msg,
)
test_cases = chain(
[
new_expression(
"Earth",
None,
"file-Earth.zip",
{"name": "file-Earth.zip"},
msg="Current system always included as an exact match synonym",
),
new_expression(
"Linux",
{"Linux": "jumanji"},
"file-jumanji.zip",
{"name": "file-jumanji.zip"},
msg="Non-standard system mapping works",
),
new_expression(
"Linux",
{},
"file-darwin.zip",
{},
ValueError,
msg="No matching system",
),
],
# Test default mappings
(
new_expression(
actual_system,
None,
file_name,
{"name": file_name},
msg="Default Linux mappings",
)
for actual_system, file_name in product(
("Linux", "linux"),
("file-Linux.zip", "file-linux.zip"),
)
),
(
new_expression(
actual_system,
None,
file_name,
{"name": file_name},
msg="Default macOS mappings",
)
for actual_system, file_name in product(
("Darwin", "darwin", "MacOS", "macos", "macOS"),
(
"file-Darwin.zip",
"file-darwin.zip",
"file-MacOS.zip",
"file-macos.zip",
),
)
),
(
new_expression(
actual_system,
None,
file_name,
{"name": file_name},
msg="Default Windows mappings",
)
for actual_system, file_name in product(
("Windows", "windows", "win", "win32", "win64"),
(
"file-Windows.zip",
"file-windows.zip",
"file-win.zip",
"file-win32.zip",
"file-win64.zip",
),
)
),
)
for test_case in test_cases:
test_case.run(first_result(run_with_context))
def test_match_asset_archs(self, *_):
# Input variations:
# Case 1: Arch mapping provided
# Case 2: No arch mapping provided
# Test: We want to show that default matching will work out of the box with some values for the current machine
# Test: We want to show that non-standard mappings will always work if provided manually
def run_with_context(actual_arch: str, *args, **kwargs):
with patch("platform.machine", return_value=actual_arch):
return release_gitter.match_asset(*args, **kwargs)
def new_expression(
actual_arch: str,
arch_mapping: dict[str, str] | None,
file_name: str,
expected: dict[str, str],
exception: type[Exception] | None = None,
msg: str | None = None,
):
release = {
"name": "v1.0.0",
"tag_name": "v1.0.0",
"assets": [{"name": file_name}],
}
return TestExpression(
self,
[actual_arch, release, "file-{arch}.zip"],
{"arch_mapping": arch_mapping},
expected,
exception,
msg,
)
test_cases = chain(
[
new_expression(
"Earth",
None,
"file-Earth.zip",
{"name": "file-Earth.zip"},
msg="Current arch always included as an exact match synonym",
),
new_expression(
"x86_64",
{"x86_64": "jumanji"},
"file-jumanji.zip",
{"name": "file-jumanji.zip"},
msg="Non-standard arch mapping works",
),
new_expression(
"x86_64",
{},
"file-arm.zip",
{},
ValueError,
msg="No matching arch",
),
],
# Test default mappings
(
new_expression(
actual_arch,
None,
file_name,
{"name": file_name},
msg="Default arm mappings",
)
for actual_arch, file_name in product(
("arm",),
("file-arm.zip",),
)
),
(
new_expression(
actual_arch,
None,
file_name,
{"name": file_name},
msg="Default amd64 mappings",
)
for actual_arch, file_name in product(
("amd64", "x86_64", "AMD64"),
("file-amd64.zip", "file-x86_64.zip"),
)
),
(
new_expression(
actual_arch,
None,
file_name,
{"name": file_name},
msg="Default arm64 mappings",
)
for actual_arch, file_name in product(
("arm64", "aarch64", "armv8b", "armv8l"),
(
"file-arm64.zip",
"file-aarch64.zip",
"file-armv8b.zip",
"file-armv8l.zip",
),
)
),
(
new_expression(
actual_arch,
None,
file_name,
{"name": file_name},
msg="Default x86 mappings",
)
for actual_arch, file_name in product(
("x86", "i386", "i686"),
("file-x86.zip", "file-i386.zip", "file-i686.zip"),
)
),
)
for test_case in test_cases:
test_case.run(first_result(run_with_context))
if __name__ == "__main__":
unittest.main()