Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
33 changes: 29 additions & 4 deletions aikido_zen/background_process/packages.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"""Helper functions for packages"""

import importlib.metadata as importlib_metadata

from packaging.version import Version
import re

import aikido_zen.helpers.get_current_unixtime_ms as t
from aikido_zen.helpers.logging import logger
Expand Down Expand Up @@ -49,9 +48,35 @@ def is_package_compatible(package=None, required_version=ANY_VERSION, packages=N
return False # We don't support it, since something unexpected happened in checking compatibility.


# PEP 440 version scheme, the same one the packaging library accepts
VERSION_PATTERN = re.compile(
r"v?(?:([0-9]+)!)?([0-9]+(?:\.[0-9]+)*)"
r"([-_.]?(?:alpha|a|beta|b|preview|pre|c|rc)[-_.]?[0-9]*)?"
r"(-[0-9]+|[-_.]?(?:post|rev|r)[-_.]?[0-9]*)?"
r"([-_.]?dev[-_.]?[0-9]*)?"
r"(?:\+[a-z0-9]+(?:[-_.][a-z0-9]+)*)?",
re.IGNORECASE,
)


def is_version_supported(version, required_version):
"""Checks if the package version is supported"""
return Version(version) >= Version(required_version)
"""Checks if the package version is supported, required_version is a final release (e.g. 2.8.6)"""
return parse_version(version) >= parse_version(required_version)


def parse_version(version):
match = VERSION_PATTERN.fullmatch(version.strip())
if not match:
raise ValueError(f"Invalid version: {version}")
epoch, release, pre, post, dev = match.groups()
numbers = [int(number) for number in release.split(".")]
while len(numbers) > 1 and numbers[-1] == 0:
numbers.pop()
# Pre-releases (2.8.6rc1) and dev releases (2.8.6.dev0) come before 2.8.6,
# but a dev release of a post-release (2.8.6.post1.dev0) comes after it.
is_dev_release = dev is not None and post is None
is_before_release = pre is not None or is_dev_release
return int(epoch or 0), numbers, not is_before_release


# packages store, uses python's built in GlobalInterpreterLock (GIL)
Expand Down
57 changes: 56 additions & 1 deletion aikido_zen/background_process/packages_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest
from unittest.mock import MagicMock
from .packages import is_package_compatible, ANY_VERSION
from .packages import is_package_compatible, is_version_supported, ANY_VERSION


@pytest.fixture
Expand Down Expand Up @@ -85,3 +85,58 @@ def test_pkg_compat_check_partial_success(mock_get_comms, mocker):

# Call the function under test
is_package_compatible(pkg_name, ANY_VERSION)


@pytest.mark.parametrize(
"version, required_version, expected",
[
("2.8.6", "2.8.6", True),
("2.8.7", "2.8.6", True),
("2.10.0", "2.8.6", True),
("3.0", "2.8.6", True),
("2.8.5", "2.8.6", False),
("2.8", "2.8.6", False),
("1.0", "1.0.0", True),
("1.0.0.0", "1.0", True),
("0.9.0", "0.0.0", True),
("2.8.6rc1", "2.8.6", False),
("2.8.6a1", "2.8.6", False),
("2.8.6.dev0", "2.8.6", False),
("2.8.6rc1.post1", "2.8.6", False),
("2.8.6.post1", "2.8.6", True),
("2.8.6.post1.dev0", "2.8.6", True),
("2.8.6-1", "2.8.6", True),
("2.8.6+ubuntu-1", "2.8.6", True),
("2.8.7rc1", "2.8.6", True),
("v2.8.6", "2.8.6", True),
(" 2.8.6 ", "2.8.6", True),
("1!0.1", "2.8.6", True),
],
)
def test_is_version_supported(version, required_version, expected):
assert is_version_supported(version, required_version) is expected


@pytest.mark.parametrize(
"version", ["2.8.6-foo", "garbage", "", "2.8.6+", "\u0663.\u0661"]
)
def test_is_version_supported_invalid_version(version):
with pytest.raises(ValueError):
is_version_supported(version, "2.8.6")


def test_pkg_compat_check_invalid_version(mocker):
mocker.patch("importlib.metadata.version", return_value="2.2.4-foo")
assert is_package_compatible("example_invalid_package", "2.2.4") is False


def test_packages_module_works_without_packaging(monkeypatch):
import importlib.util
import sys

monkeypatch.setitem(sys.modules, "packaging", None)
monkeypatch.setitem(sys.modules, "packaging.version", None)
spec = importlib.util.find_spec("aikido_zen.background_process.packages")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
assert module.is_version_supported("3.1.0", "2.2.4") is True
4 changes: 2 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ regex = [
{ version = "^2024.5.15", python = "<3.13" },
{ version = "^2026.8.31", python = ">=3.13" },
]
packaging = "^24.1"
wrapt = [
{ version = "^1.17.2", python = "<3.13" },
{ version = "^2.4.0", python = ">=3.13" },
Expand Down
Loading