Skip to content
Merged
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
46 changes: 14 additions & 32 deletions sdk/src/opendecree/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

from __future__ import annotations

import re
from typing import Any

from packaging.specifiers import InvalidSpecifier, Specifier
from packaging.version import InvalidVersion, Version

import opendecree
from opendecree.errors import IncompatibleServerError
from opendecree.types import ServerVersion
Expand Down Expand Up @@ -64,44 +66,24 @@ def check_version_compatible(server_version: str, supported_range: str | None =
return

for constraint in supported_range.split(","):
constraint = constraint.strip()
if not _satisfies(parsed, constraint):
if not _satisfies(parsed, constraint.strip()):
raise IncompatibleServerError(
f"Server version {server_version} is not compatible with this SDK "
f"(requires {supported_range})"
)


def _parse_version(version: str) -> tuple[int, ...] | None:
"""Parse a semver string into a tuple of ints, or None if unparseable."""
match = re.match(r"^v?(\d+(?:\.\d+)*)", version)
if not match:
def _parse_version(version: str) -> Version | None:
"""Parse a version string via PEP 440, or None if unparseable."""
try:
return Version(version)
except InvalidVersion:
return None
return tuple(int(p) for p in match.group(1).split("."))


def _satisfies(version: tuple[int, ...], constraint: str) -> bool:
"""Check if a version tuple satisfies a single constraint like '>=0.3.0'."""
match = re.match(r"^(>=|<=|>|<|==|!=)(.+)$", constraint)
if not match:
return True

op = match.group(1)
target = _parse_version(match.group(2))
if target is None:
def _satisfies(version: Version, constraint: str) -> bool:
"""Check if a Version satisfies a single constraint like '>=0.3.0'."""
try:
return version in Specifier(constraint, prereleases=True)
except InvalidSpecifier:
return True

# Pad to same length for comparison.
max_len = max(len(version), len(target))
v = version + (0,) * (max_len - len(version))
t = target + (0,) * (max_len - len(target))

ops = {
">=": v >= t,
"<=": v <= t,
">": v > t,
"<": v < t,
"==": v == t,
"!=": v != t,
}
return ops[op]
48 changes: 31 additions & 17 deletions sdk/tests/test_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from packaging.version import Version

from opendecree._compat import (
_parse_version,
Expand All @@ -18,15 +19,25 @@


def test_parse_semver():
assert _parse_version("0.3.1") == (0, 3, 1)
assert _parse_version("0.3.1") == Version("0.3.1")


def test_parse_with_v_prefix():
assert _parse_version("v1.2.3") == (1, 2, 3)
assert _parse_version("v1.2.3") == Version("1.2.3")


def test_parse_major_only():
assert _parse_version("2") == (2,)
assert _parse_version("2") == Version("2")


def test_parse_prerelease():
assert _parse_version("0.3.0-rc1") == Version("0.3.0rc1")


def test_parse_local_version():
v = _parse_version("v0.3.0+sha123")
assert v is not None
assert v == Version("0.3.0+sha123")


def test_parse_dev():
Expand All @@ -41,34 +52,37 @@ def test_parse_empty():


def test_satisfies_gte():
assert _satisfies((0, 3, 1), ">=0.3.0") is True
assert _satisfies((0, 3, 0), ">=0.3.0") is True
assert _satisfies((0, 2, 9), ">=0.3.0") is False
assert _satisfies(Version("0.3.1"), ">=0.3.0") is True
assert _satisfies(Version("0.3.0"), ">=0.3.0") is True
assert _satisfies(Version("0.2.9"), ">=0.3.0") is False


def test_satisfies_lt():
assert _satisfies((0, 9, 0), "<1.0.0") is True
assert _satisfies((1, 0, 0), "<1.0.0") is False
assert _satisfies(Version("0.9.0"), "<1.0.0") is True
assert _satisfies(Version("1.0.0"), "<1.0.0") is False


def test_satisfies_eq():
assert _satisfies((1, 2, 3), "==1.2.3") is True
assert _satisfies((1, 2, 4), "==1.2.3") is False
assert _satisfies(Version("1.2.3"), "==1.2.3") is True
assert _satisfies(Version("1.2.4"), "==1.2.3") is False


def test_satisfies_neq():
assert _satisfies((1, 2, 4), "!=1.2.3") is True
assert _satisfies((1, 2, 3), "!=1.2.3") is False
assert _satisfies(Version("1.2.4"), "!=1.2.3") is True
assert _satisfies(Version("1.2.3"), "!=1.2.3") is False


def test_satisfies_short_version():
assert _satisfies(Version("1"), ">=1.0.0") is True
assert _satisfies(Version("1"), "<2.0.0") is True


def test_satisfies_padding():
"""Shorter version tuples are padded with zeros."""
assert _satisfies((1,), ">=1.0.0") is True
assert _satisfies((1,), "<2.0.0") is True
def test_satisfies_prerelease_lt_release():
assert _satisfies(Version("0.3.0rc1"), ">=0.3.0") is False


def test_satisfies_invalid_constraint():
assert _satisfies((1, 0, 0), "garbage") is True
assert _satisfies(Version("1.0.0"), "garbage") is True


# --- check_version_compatible ---
Expand Down