diff --git a/albumentations/check_version.py b/albumentations/check_version.py index 59d5c3b4f..ff398d5ee 100644 --- a/albumentations/check_version.py +++ b/albumentations/check_version.py @@ -1,4 +1,7 @@ +from __future__ import annotations + import json +import re import urllib.request from urllib.request import OpenerDirector from warnings import warn @@ -46,21 +49,77 @@ def parse_version(data: str) -> str: return "" +def compare_versions(v1: tuple[int | str, ...], v2: tuple[int | str, ...]) -> bool: + """Compare two version tuples. + Returns True if v1 > v2, False otherwise. + + Special rules: + 1. Release version > pre-release version (e.g., (1, 4) > (1, 4, 'beta')) + 2. Numeric parts are compared numerically + 3. String parts are compared lexicographically + """ + # First compare common parts + for p1, p2 in zip(v1, v2): + if p1 != p2: + # If both are same type, direct comparison works + if isinstance(p1, int) and isinstance(p2, int): + return p1 > p2 + if isinstance(p1, str) and isinstance(p2, str): + return p1 > p2 + # If types differ, numbers are greater (release > pre-release) + return isinstance(p1, int) + + # If we get here, all common parts are equal + # Longer version is greater only if next element is a number + if len(v1) > len(v2): + return isinstance(v1[len(v2)], int) + if len(v2) > len(v1): + # v2 is longer, so v1 is greater only if v2's next part is a string (pre-release) + return isinstance(v2[len(v1)], str) + + return False # Versions are equal + + +def parse_version_parts(version_str: str) -> tuple[int | str, ...]: + """Convert version string to tuple of (int | str) parts following PEP 440 conventions. + + Examples: + "1.4.24" -> (1, 4, 24) + "1.4beta" -> (1, 4, "beta") + "1.4.beta2" -> (1, 4, "beta", 2) + "1.4.alpha2" -> (1, 4, "alpha", 2) + """ + parts = [] + # First split by dots + for part in version_str.split("."): + # Then parse each part for numbers and letters + segments = re.findall(r"([0-9]+|[a-zA-Z]+)", part) + for segment in segments: + if segment.isdigit(): + parts.append(int(segment)) + else: + parts.append(segment.lower()) + return tuple(parts) + + def check_for_updates() -> None: try: data = fetch_version_info() latest_version = parse_version(data) - if latest_version and latest_version != current_version: - warn( - f"A new version of Albumentations is available: {latest_version} (you have {current_version}). " # noqa: S608 - "Upgrade using: pip install -U albumentations. " - "To disable automatic update checks, set the environment variable NO_ALBUMENTATIONS_UPDATE to 1.", - UserWarning, - stacklevel=2, - ) - except Exception as e: # General exception catch to ensure silent failure # noqa: BLE001 + if latest_version: + latest_parts = parse_version_parts(latest_version) + current_parts = parse_version_parts(current_version) + if compare_versions(latest_parts, current_parts): + warn( + f"A new version of Albumentations is available: {latest_version!r} (you have {current_version!r}). " + "Upgrade using: pip install -U albumentations. " + "To disable automatic update checks, set the environment variable NO_ALBUMENTATIONS_UPDATE to 1.", + UserWarning, + stacklevel=2, + ) + except Exception as e: # General exception catch to ensure silent failure # noqa: BLE001 warn( - f"Failed to check for updates due to an unexpected error: {e}. " # noqa: S608 + f"Failed to check for updates due to error: {e}. " "To disable automatic update checks, set the environment variable NO_ALBUMENTATIONS_UPDATE to 1.", UserWarning, stacklevel=2, diff --git a/pyproject.toml b/pyproject.toml index 4bbd687c1..8adf88540 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ requires = [ "setuptools>=45", "wheel" ] [project] name = "albumentations" -version = "1.5.0" +version = "2.0.0" description = "Fast, flexible, and advanced augmentation library for deep learning, computer vision, and medical imaging. Albumentations offers a wide range of transformations for both 2D (images, masks, bboxes, keypoints) and 3D (volumes, volumetric masks) data, with optimized performance and seamless integration into ML workflows." readme = "README.md" @@ -225,6 +225,7 @@ lint.ignore = [ "PLR2004", "PTH123", "S311", + "S608", "TC001", "TC002", "TC003", diff --git a/tests/test_check_version.py b/tests/test_check_version.py index 107f54720..aac583992 100644 --- a/tests/test_check_version.py +++ b/tests/test_check_version.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest.mock import MagicMock, patch import pytest @@ -7,6 +9,8 @@ fetch_version_info, get_opener, parse_version, + parse_version_parts, + compare_versions, ) @@ -127,3 +131,47 @@ def test_check_for_updates_with_update(): with patch("albumentations.check_version.warn") as mock_warn: # Patch the imported warn check_for_updates() mock_warn.assert_called_once() + + + +@pytest.mark.parametrize("version_str, expected", [ + # Standard versions + ("1.4.24", (1, 4, 24)), + ("0.0.1", (0, 0, 1)), + ("10.20.30", (10, 20, 30)), + + # Pre-release versions + ("1.4beta", (1, 4, "beta")), + ("1.4beta2", (1, 4, "beta", 2)), + ("1.4.beta2", (1, 4, "beta", 2)), + ("1.4.alpha2", (1, 4, "alpha", 2)), + ("1.4rc1", (1, 4, "rc", 1)), + ("1.4.rc.1", (1, 4, "rc", 1)), + + # Mixed case handling + ("1.4Beta2", (1, 4, "beta", 2)), + ("1.4ALPHA2", (1, 4, "alpha", 2)), +]) +def test_parse_version_parts(version_str: str, expected: tuple[int | str, ...]) -> None: + assert parse_version_parts(version_str) == expected + +# Update the test to use the new comparison function +@pytest.mark.parametrize("version1, version2, expected", [ + # Pre-release ordering + ("1.4beta2", "1.4beta1", True), + ("1.4", "1.4beta", True), + ("1.4beta", "1.4alpha", True), + ("1.4alpha2", "1.4alpha1", True), + ("1.4rc", "1.4beta", True), + ("2.0", "2.0rc1", True), + + # Standard version ordering + ("1.5", "1.4", True), + ("1.4.1", "1.4", True), + ("1.4.24", "1.4.23", True), +]) +def test_version_comparison(version1: str, version2: str, expected: bool) -> None: + """Test that version1 > version2 matches expected result.""" + v1 = parse_version_parts(version1) + v2 = parse_version_parts(version2) + assert compare_versions(v1, v2) == expected