Compare commits

..

1 Commits

Author SHA1 Message Date
IamTheFij 2ece5aa3c9 Use new type annotations introduced in Python 3.10
continuous-integration/drone/push Build is passing Details
2022-09-01 12:29:10 -07:00
12 changed files with 187 additions and 654 deletions

View File

@ -5,8 +5,6 @@ PYTHON_VERSIONS = [
"3.8",
"3.9",
"3.10",
"3.11",
"3.12",
"latest",
]
@ -16,19 +14,6 @@ def main(ctx):
# Run tests
pipelines += tests()
pipelines += [{
"kind": "pipeline",
"name": "lint",
"workspace": get_workspace(),
"steps": [{
"name": "lint",
"image": "python:3",
"commands": [
"python -V",
"make lint",
]
}]
}]
# Add pypi push pipeline
pipelines += push_to_pypi()
@ -57,20 +42,24 @@ def tests():
"name": "tests",
"workspace": get_workspace(),
"steps": [
test_step("python:"+version)
tox_step("python:"+version)
for version in PYTHON_VERSIONS
],
}]
# Builds a single python test step
def test_step(docker_tag, python_cmd="python"):
def tox_step(docker_tag, python_cmd="python", tox_env="py3"):
return {
"name": "test {}".format(docker_tag.replace(":", "")),
"image": docker_tag,
"environment": {
"TOXENV": tox_env,
},
"commands": [
"{} -V".format(python_cmd),
"make clean-all test"
"pip install tox",
"tox",
],
}
@ -120,36 +109,36 @@ def push_to_pypi():
return [{
"kind": "pipeline",
"name": "deploy to pypi",
"depends_on": ["tests", "lint"],
"depends_on": ["tests"],
"workspace": get_workspace(),
"trigger": {
"ref": [
# "refs/heads/main",
"refs/heads/main",
"refs/tags/v*",
],
},
"steps": [
# {
# "name": "push to test pypi",
# "image": "python:3",
# "environment": {
# "HATCH_INDEX_USER": {
# "from_secret": "PYPI_USERNAME",
# },
# "HATCH_INDEX_AUTH": {
# "from_secret": "TEST_PYPI_PASSWORD",
# },
# },
# "commands": ["make upload-test"],
# },
{
"name": "push to test pypi",
"image": "python:3",
"environment": {
"TWINE_USERNAME": {
"from_secret": "PYPI_USERNAME",
},
"TWINE_PASSWORD": {
"from_secret": "TEST_PYPI_PASSWORD",
},
},
"commands": ["make upload-test"],
},
{
"name": "push to pypi",
"image": "python:3",
"environment": {
"HATCH_INDEX_USER": {
"TWINE_USERNAME": {
"from_secret": "PYPI_USERNAME",
},
"HATCH_INDEX_AUTH": {
"TWINE_PASSWORD": {
"from_secret": "PYPI_PASSWORD",
},
},

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

@ -5,7 +5,7 @@ ENV := venv
.PHONY: default
default: test
# Creates de virtualenv
# Creates virtualenv
$(ENV):
python3 -m venv $(ENV)
@ -13,76 +13,87 @@ $(ENV):
$(ENV)/bin/$(NAME): $(ENV)
$(ENV)/bin/pip install -r requirements-dev.txt
# Install hatch into virtualenv for running tests
$(ENV)/bin/hatch: $(ENV)
$(ENV)/bin/pip install hatch
# Install tox into virtualenv for running tests
$(ENV)/bin/tox: $(ENV)
$(ENV)/bin/pip install tox
# Install wheel for building packages
$(ENV)/bin/wheel: $(ENV)
$(ENV)/bin/pip install wheel
# Install twine for uploading packages
$(ENV)/bin/twine: $(ENV)
$(ENV)/bin/pip install twine
# Installs dev requirements to virtualenv
.PHONY: devenv
devenv: $(ENV)/bin/$(NAME)
# Runs tests for current python
.PHONY: test
test: $(ENV)/bin/hatch
$(ENV)/bin/hatch run +py=3 test:run
# Generates a smaller env for running tox, which builds it's own env
.PHONY: test-env
test-env: $(ENV)/bin/tox
# Runs test matrix
.PHONY: test-matrix
test-matrix: $(ENV)/bin/hatch
$(ENV)/bin/hatch run test:run
# Generates a small build env for building and uploading dists
.PHONY: build-env
build-env: $(ENV)/bin/twine $(ENV)/bin/wheel
# Runs package
.PHONY: run
run: $(ENV)/bin/$(NAME)
$(ENV)/bin/$(NAME)
# Runs tests with tox
.PHONY: test
test: $(ENV)/bin/tox
$(ENV)/bin/tox
# Builds wheel for package to upload
.PHONY: build
build: $(ENV)/bin/hatch
$(ENV)/bin/hatch build
build: $(ENV)/bin/wheel
$(ENV)/bin/python setup.py sdist
$(ENV)/bin/python setup.py bdist_wheel
# Verify that the python version matches the git tag so we don't push bad shas
.PHONY: verify-tag-version
verify-tag-version: $(ENV)/bin/hatch
verify-tag-version:
$(eval TAG_NAME = $(shell [ -n "$(DRONE_TAG)" ] && echo $(DRONE_TAG) || git describe --tags --exact-match))
test "v$(shell $(ENV)/bin/hatch version)" = "$(TAG_NAME)"
test "v$(shell python setup.py -V)" = "$(TAG_NAME)"
# Upload to pypi
# Uses twine to upload to pypi
.PHONY: upload
upload: verify-tag-version build
$(ENV)/bin/hatch publish
upload: verify-tag-version build $(ENV)/bin/twine
$(ENV)/bin/twine upload dist/*
# Uses twine to upload to test pypi
.PHONY: upload-test
upload-test: build
# Bump version to a post version based on num of commits since last tag to prevent overwriting
$(ENV)/bin/hatch version $(shell git describe --tags | sed 's/-[0-9a-z]*$$//')
$(ENV)/bin/hatch publish --repo test
upload-test: build $(ENV)/bin/twine
$(ENV)/bin/twine check dist/*
$(ENV)/bin/twine upload --skip-existing --repository-url https://test.pypi.org/legacy/ dist/*
# Cleans all build, runtime, and test artifacts
.PHONY: clean
clean:
rm -fr ./build *.egg-info ./htmlcov ./.coverage ./.pytest_cache
rm -fr ./build *.egg-info ./htmlcov ./.coverage ./.pytest_cache ./.tox
find . -name '*.pyc' -delete
find . -name '__pycache__' -delete
# Cleans dist and env
.PHONY: dist-clean
dist-clean: clean
-$(ENV)/bin/hatch env prune
rm -fr ./dist $(ENV)
# Run linters
.PHONY: lint
lint: $(ENV)/bin/hatch
$(ENV)/bin/hatch run lint:all
# Install pre-commit hooks
.PHONY: install-hooks
install-hooks: devenv
$(ENV)/bin/hatch run lint:install-hooks
$(ENV)/bin/pre-commit install -f --install-hooks
# Generates test coverage
.coverage: test
.coverage:
$(ENV)/bin/tox
# Builds coverage html
htmlcov/index.html: .coverage
$(ENV)/bin/hatch run coverage html
$(ENV)/bin/coverage html
# Opens coverage html in browser (on macOS and some Linux systems)
.PHONY: open-coverage
@ -96,7 +107,7 @@ docs-clean:
# Builds docs
docs/build/html/index.html:
$(ENV)/bin/hatch run docs:build
$(ENV)/bin/tox -e docs
# Shorthand for building docs
.PHONY: 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,12 +2,9 @@
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
@ -17,29 +14,11 @@ from wheel.wheelfile import WheelFile
import release_gitter as rg
from release_gitter import removeprefix
PACKAGE_NAME = "pseudo"
@dataclass
class Config:
format: str
git_url: str
hostname: str
owner: str
repo: str
version: str | None = None
pre_release: bool = False
version_git_tag: bool = False
version_git_no_fetch: bool = False
map_system: dict[str, str] | None = None
map_arch: dict[str, str] | None = None
exec: str | None = None
extract_all: bool = False
extract_files: list[str] | None = None
include_extra_files: list[str] | None = None
def download(config: Config) -> list[Path]:
def download(config) -> list[Path]:
release = rg.fetch_release(
rg.GitRemoteInfo(config.hostname, config.owner, config.repo), config.version
)
@ -60,35 +39,26 @@ def download(config: Config) -> list[Path]:
return files
def read_metadata() -> Config:
def read_metadata():
config = toml.load("pyproject.toml").get("tool", {}).get("release-gitter")
if not config:
raise ValueError("Must have configuration in [tool.release-gitter]")
git_url = config.pop("git-url", None)
remote_info = rg.parse_git_remote(git_url)
args = Config(
format=config.pop("format"),
git_url=git_url,
hostname=config.pop("hostname", remote_info.hostname),
owner=config.pop("owner", remote_info.owner),
repo=config.pop("repo", remote_info.repo),
)
args = []
for key, value in config.items():
setattr(args, str(key).replace("-", "_"), value)
key = "--" + key
if key == "--format":
args += [value]
elif isinstance(value, dict):
for sub_key, sub_value in value.items():
args = [key, f"{sub_key}={sub_value}"] + args
elif isinstance(value, list):
for sub_value in value:
args = [key, sub_value] + args
else:
args = [key, value] + args
if args.version is None:
args.version = rg.read_version(
args.version_git_tag,
not args.version_git_no_fetch,
)
if args.extract_all:
args.extract_files = []
return args
return rg._parse_args(args)
class _PseudoBuildBackend:
@ -98,11 +68,11 @@ class _PseudoBuildBackend:
def prepare_metadata_for_build_wheel(
self, metadata_directory, config_settings=None
):
# Create a .dist-info directory containing wheel metadata inside metadata_directory. Eg {metadata_directory}/{package}-{version}.dist-info/
# Createa .dist-info directory containing wheel metadata inside metadata_directory. Eg {metadata_directory}/{package}-{version}.dist-info/
print("Prepare meta", metadata_directory, config_settings)
metadata = read_metadata()
version = removeprefix(metadata.version, "v") if metadata.version else "0.0.0"
version = removeprefix(metadata.version, "v")
# Returns distinfo dir?
dist_info = Path(metadata_directory) / f"{PACKAGE_NAME}-{version}.dist-info"
@ -149,7 +119,7 @@ class _PseudoBuildBackend:
metadata_directory = Path(metadata_directory)
metadata = read_metadata()
version = removeprefix(metadata.version, "v") if metadata.version else "0.0.0"
version = removeprefix(metadata.version, "v")
wheel_directory = Path(wheel_directory)
wheel_directory.mkdir(exist_ok=True)
@ -164,16 +134,7 @@ class _PseudoBuildBackend:
for file in files:
move(file, wheel_scripts / file.name)
for file_name in metadata.include_extra_files or []:
file = Path(file_name)
if Path.cwd() in file.absolute().parents:
copy(file_name, wheel_scripts / file)
else:
raise ValueError(
f"Cannot include any path that is not within the current directory: {file_name}"
)
print(f"ls {wheel_directory}: {list(wheel_directory.rglob('*'))}")
print(f"ls {wheel_directory}: {list(wheel_directory.glob('*'))}")
wheel_filename = f"{PACKAGE_NAME}-{version}-py2.py3-none-any.whl"
with WheelFile(wheel_directory / wheel_filename, "w") as wf:

View File

@ -1,69 +0,0 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "release-gitter"
dynamic = ["version"]
description = "Easily download releases from sites like Github and Gitea"
readme = "README.md"
license = "MIT"
classifiers = [
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
"License :: OSI Approved :: MIT License",
]
authors = [
{ name = "Ian Fijolek", email = "iamthefij@gmail.com" }
]
maintainers = [
{ name = "Ian Fijolek", email = "iamthefij@gmail.com" }
]
requires-python = ">=3.7"
dependencies = ["requests"]
[project.optional-dependencies]
builder = [
"toml",
"wheel",
]
[project.scripts]
release-gitter = "release_gitter:main"
[project.urls]
Homepage = "https://git.iamthefij.com/iamthefij/release-gitter"
[tool.hatch.version]
path = "release_gitter.py"
[tool.hatch.build]
include = ["release_gitter.py", "pseudo_builder.py"]
[tool.hatch.envs.test]
dependencies = [
"coverage",
]
[tool.hatch.envs.test.scripts]
run = [
"coverage erase",
"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"]
[tool.hatch.envs.lint]
detached = true
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

@ -6,7 +6,6 @@ import platform
from collections.abc import Sequence
from dataclasses import dataclass
from io import BytesIO
from itertools import product
from mimetypes import guess_type
from pathlib import Path
from subprocess import check_call
@ -19,12 +18,6 @@ from zipfile import ZipFile
import requests
__version__ = "2.3.0"
class UnsupportedContentTypeError(ValueError):
pass
class InvalidRemoteError(ValueError):
pass
@ -48,31 +41,6 @@ def removesuffix(s: str, suf: str) -> str:
return s[: -len(suf)] if s and s.endswith(suf) else s
SYSTEM_SYNONYMS: list[list[str]] = [
["Darwin", "darwin", "MacOS", "macos", "macOS"],
["Windows", "windows", "win", "win32", "win64"],
["Linux", "linux"],
]
ARCH_SYNONYMS: list[list[str]] = [
["arm"],
["x86_64", "amd64", "AMD64"],
["arm64", "aarch64", "armv8b", "armv8l"],
["x86", "i386", "i686"],
]
def get_synonyms(value: str, thesaurus: list[list[str]]) -> list[str]:
"""Gets synonym list for a given value."""
results = [value]
for l in thesaurus:
if value in l:
results += l
return results
@dataclass
class GitRemoteInfo:
"""Extracts information about a repository"""
@ -185,8 +153,8 @@ def read_version(from_tags: bool = False, fetch: bool = False) -> str | None:
def fetch_release(
remote: GitRemoteInfo,
version: str | None = None,
pre_release=False,
version: str | None = None
# TODO: Accept an argument for pre-release
) -> dict[Any, Any]:
"""Fetches a release object from a Github repo
@ -202,21 +170,14 @@ def fetch_release(
# Return the latest if requested
if version is None or version == "latest":
for release in result.json():
if release["prerelease"] and not pre_release:
continue
return release
return result.json()[0]
# Return matching version
for release in result.json():
if release["tag_name"].endswith(version):
return release
raise ValueError(
f"Could not find release version ending in {version}."
f"{ ' Is it a pre-release?' if not pre_release else ''}"
)
raise ValueError(f"Could not find release version ending in {version}")
def match_asset(
@ -271,29 +232,21 @@ def match_asset(
system = platform.system()
if system_mapping:
systems = [system_mapping.get(system, system)]
else:
systems = get_synonyms(system, SYSTEM_SYNONYMS)
system = system_mapping.get(system, system)
arch = platform.machine()
if arch_mapping:
archs = [arch_mapping.get(arch, arch)]
else:
archs = get_synonyms(arch, ARCH_SYNONYMS)
arch = arch_mapping.get(arch, arch)
expected_names = {
format.format(
version=version_opt,
system=system_opt,
arch=arch_opt,
version=normalized_version,
system=system,
arch=arch,
)
for version_opt, system_opt, arch_opt in product(
(
version.lstrip("v"),
"v" + version if not version.startswith("v") else version,
),
systems,
archs,
for normalized_version in (
version.lstrip("v"),
"v" + version if not version.startswith("v") else version,
)
}
@ -321,14 +274,11 @@ class PackageAdapter:
elif content_type in (
"application/gzip",
"application/x-tar+gzip",
"application/x-tar+xz",
"application/x-compressed-tar",
):
self._package = TarFile.open(fileobj=BytesIO(response.content), mode="r:*")
else:
raise UnsupportedContentTypeError(
f"Unknown or unsupported content type {content_type}"
)
raise ValueError(f"Unknown or unsupported content type {content_type}")
def get_names(self) -> list[str]:
"""Get list of all file names in package"""
@ -355,10 +305,6 @@ class PackageAdapter:
self._package.extractall(path=path)
return self.get_names()
missing_members = set(members) - set(self.get_names())
if missing_members:
raise ValueError(f"Missing members: {missing_members}")
if isinstance(self._package, ZipFile):
self._package.extractall(path=path, members=members)
if isinstance(self._package, TarFile):
@ -369,27 +315,6 @@ class PackageAdapter:
return members
def get_asset_package(
asset: dict[str, Any], result: requests.Response
) -> PackageAdapter:
possible_content_types = (
asset.get("content_type"),
"+".join(t for t in guess_type(asset["name"]) if t is not None),
)
for content_type in possible_content_types:
if not content_type:
continue
try:
return PackageAdapter(content_type, result)
except UnsupportedContentTypeError:
continue
else:
raise UnsupportedContentTypeError(
"Cannot extract files from archive because we don't recognize the content type"
)
def download_asset(
asset: dict[Any, Any],
extract_files: list[str] | None = None,
@ -412,8 +337,18 @@ def download_asset(
result = requests.get(asset["browser_download_url"])
content_type = asset.get(
"content_type",
guess_type(asset["name"]),
)
if extract_files is not None:
package = get_asset_package(asset, result)
if isinstance(content_type, tuple):
content_type = "+".join(t for t in content_type if t is not None)
if not content_type:
raise TypeError(
"Cannot extract files from archive because we don't recognize the content type"
)
package = PackageAdapter(content_type, result)
extract_files = package.extractall(path=destination, members=extract_files)
return [destination / name for name in extract_files]
@ -474,7 +409,6 @@ 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="store_true", help="verbose logging")
parser.add_argument(
"--hostname",
help="Git repository hostname",
@ -493,12 +427,7 @@ def _parse_args(args: list[str] | None = None) -> argparse.Namespace:
)
parser.add_argument(
"--version",
help="Release version to download. If not provided, it will look for project metadata",
)
parser.add_argument(
"--prerelease",
action="store_true",
help="Include pre-release versions in search",
help="Release version to download. If not provied, it will look for project metadata",
)
parser.add_argument(
"--version-git-tag",
@ -532,13 +461,13 @@ def _parse_args(args: list[str] | None = None) -> argparse.Namespace:
"--extract-files",
"-e",
action="append",
help="A list of file names to extract from the downloaded archive",
help="A list of file names to extract from downloaded archive",
)
parser.add_argument(
"--extract-all",
"-x",
action="store_true",
help="Extract all files from the downloaded archive",
help="Shell commands to execute after download or extraction",
)
parser.add_argument(
"--url-only",
@ -580,14 +509,9 @@ def download_release(
system_mapping: dict[str, str] | None = None,
arch_mapping: dict[str, str] | None = None,
extract_files: list[str] | None = None,
pre_release=False,
) -> list[Path]:
"""Convenience method for fetching, downloading and extracting a release"""
release = fetch_release(
remote_info,
version=version,
pre_release=pre_release,
)
release = fetch_release(remote_info)
asset = match_asset(
release,
format,
@ -608,9 +532,7 @@ def main():
args = _parse_args()
release = fetch_release(
GitRemoteInfo(args.hostname, args.owner, args.repo),
version=args.version,
pre_release=args.prerelease,
GitRemoteInfo(args.hostname, args.owner, args.repo), args.version
)
asset = match_asset(
release,
@ -620,9 +542,6 @@ def main():
arch_mapping=args.map_arch,
)
if args.v:
print(f"Downloading {asset['name']} from release {release['name']}")
if args.url_only:
print(asset["browser_download_url"])
return

View File

@ -1,16 +1,14 @@
from __future__ import annotations
import unittest
from itertools import chain
from itertools import product
from tarfile import TarFile
from pathlib import Path
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
from zipfile import ZipFile
import requests
@ -22,11 +20,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(
@ -144,332 +141,5 @@ class TestVersionInfo(unittest.TestCase):
release_gitter.read_version()
@patch("release_gitter.ZipFile", autospec=True)
@patch("release_gitter.BytesIO", autospec=True)
class TestContentTypeDetection(unittest.TestCase):
def test_asset_encoding_priority(self, *_):
package = release_gitter.get_asset_package(
{
"content_type": "application/x-tar",
"name": "test.zip",
},
MagicMock(spec=["raw", "content"]),
)
# Tar should take priority over the file name zip extension
self.assertIsInstance(package._package, TarFile)
def test_fallback_to_supported_encoding(self, *_):
package = release_gitter.get_asset_package(
{
"content_type": "application/octetstream",
"name": "test.zip",
},
MagicMock(spec=["raw", "content"]),
)
# Should fall back to zip extension
self.assertIsInstance(package._package, ZipFile)
def test_missing_only_name_content_type(self, *_):
package = release_gitter.get_asset_package(
{
"name": "test.zip",
},
MagicMock(spec=["raw", "content"]),
)
# Should fall back to zip extension
self.assertIsInstance(package._package, ZipFile)
def test_no_content_types(self, *_):
with self.assertRaises(release_gitter.UnsupportedContentTypeError):
release_gitter.get_asset_package(
{
"name": "test",
},
MagicMock(spec=["raw", "content"]),
)
def test_no_supported_content_types(self, *_):
with self.assertRaises(release_gitter.UnsupportedContentTypeError):
release_gitter.get_asset_package(
{
"content_type": "application/octetstream",
"name": "test",
},
MagicMock(spec=["raw", "content"]),
)
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(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(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(run_with_context)
if __name__ == "__main__":
unittest.main()

View File

@ -1,6 +1,4 @@
-e .
hatch
mypy
pytest
coverage
pre-commit
types-requests
types-toml

View File

@ -11,7 +11,6 @@ version = "0.11.3"
extract-files = [ "stylua" ]
format = "stylua-{version}-{system}.zip"
exec = "chmod +x stylua"
[tool.release-gitter.map-system]
Darwin = "macos"
Windows = "win64"

40
setup.py Normal file
View File

@ -0,0 +1,40 @@
from codecs import open
from os import path
from setuptools import find_packages
from setuptools import setup
here = path.abspath(path.dirname(__file__))
# Get the long description from the README file
with open(path.join(here, "README.md"), encoding="utf-8") as f:
long_description = f.read()
setup(
name="release-gitter",
version="1.1.3",
description="Easily download releases from sites like Github and Gitea",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://git.iamthefij.com/iamthefij/release-gitter.git",
download_url=(
"https://git.iamthefij.com/iamthefij/release-gitter.git/archive/master.tar.gz"
),
author="iamthefij",
author_email="",
classifiers=[
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
],
keywords="",
py_modules=["release_gitter", "pseudo_builder"],
install_requires=["requests"],
extras_require={"builder": ["toml", "wheel"]},
entry_points={
"console_scripts": [
"release-gitter=release_gitter:main",
],
},
)

17
tox.ini Normal file
View File

@ -0,0 +1,17 @@
[tox]
envlist = py3,py37,py38,py39,py310
[testenv]
deps =
-rrequirements-dev.txt
commands =
coverage erase
coverage run --source=release_gitter -m unittest discover . {posargs:"*_test.py"}
coverage report -m # --fail-under 70
pre-commit run --all-files
[testenv:pre-commit]
deps =
pre-commit
commands =
pre-commit {posargs}