From a388077fb1facb55911109bad6d3325e93d3c02e Mon Sep 17 00:00:00 2001 From: Daniel Iulian Iacob Date: Fri, 23 Oct 2026 16:19:06 +0200 Subject: [PATCH 1/2] Drop the packaging runtime dependency --- aikido_zen/background_process/packages.py | 31 ++++++++-- .../background_process/packages_test.py | 57 ++++++++++++++++++- poetry.lock | 4 +- pyproject.toml | 1 - 4 files changed, 85 insertions(+), 8 deletions(-) diff --git a/aikido_zen/background_process/packages.py b/aikido_zen/background_process/packages.py index 6b744bb9d..ace5c9816 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,33 @@ 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 itself + is_final_release = pre is None and (dev is None or post is not None) + return int(epoch or 0), numbers, is_final_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" }, From 6079e2d538d1701ea638bcca2d0c6cfde0d1897a Mon Sep 17 00:00:00 2001 From: Daniel Iulian Iacob Date: Fri, 23 Oct 2026 17:35:28 +0200 Subject: [PATCH 2/2] Split the final-release check into named conditions --- aikido_zen/background_process/packages.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/aikido_zen/background_process/packages.py b/aikido_zen/background_process/packages.py index ace5c9816..f2978d54d 100644 --- a/aikido_zen/background_process/packages.py +++ b/aikido_zen/background_process/packages.py @@ -72,9 +72,11 @@ def parse_version(version): 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 itself - is_final_release = pre is None and (dev is None or post is not None) - return int(epoch or 0), numbers, is_final_release + # 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)