diff --git a/aikido_zen/background_process/packages.py b/aikido_zen/background_process/packages.py index 6b744bb9d..f2978d54d 100644 --- a/aikido_zen/background_process/packages.py +++ b/aikido_zen/background_process/packages.py @@ -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 @@ -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) diff --git a/aikido_zen/background_process/packages_test.py b/aikido_zen/background_process/packages_test.py index 66e3419a4..ca922f7a1 100644 --- a/aikido_zen/background_process/packages_test.py +++ b/aikido_zen/background_process/packages_test.py @@ -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 @@ -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 diff --git a/poetry.lock b/poetry.lock index 0d0abfc07..bc6096dbc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2267,7 +2267,7 @@ version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -4238,4 +4238,4 @@ cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and pyt [metadata] lock-version = "2.1" python-versions = ">=3.8,<3.15" -content-hash = "c77b3d119232b6bf287feec1ce02bee84defa04c192f3d5e2a7179358591a3ef" +content-hash = "456651d105d4f0d5eadca0bd6ff4e0ad7d4c5300385c26077bca8a8066877215" diff --git a/pyproject.toml b/pyproject.toml index 7ea7eb753..ec2575089 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" },