From 941fb9c3fc1f885f9e961151d13b79bf0e0d004a Mon Sep 17 00:00:00 2001 From: "Kunz, Immanuel" Date: Wed, 20 Nov 2024 08:15:41 +0100 Subject: [PATCH 1/8] first libversion draft Signed-off-by: Kunz, Immanuel --- src/univers/libversion.py | 138 ++++++++++++++++++++++++++++++++++++ src/univers/versions.py | 11 +++ tests/test_version_range.py | 14 ++++ tests/test_versions.py | 10 +++ 4 files changed, 173 insertions(+) create mode 100644 src/univers/libversion.py diff --git a/src/univers/libversion.py b/src/univers/libversion.py new file mode 100644 index 00000000..4605e522 --- /dev/null +++ b/src/univers/libversion.py @@ -0,0 +1,138 @@ +import re + +PRE_RELEASE_KEYWORDS = ["alpha", "beta", "rc", "pre"] +POST_RELEASE_KEYWORDS = ["post", "patch", "pl", "errata"] + +KEYWORD_UNKNOWN = 0 +KEYWORD_PRE_RELEASE = 1 +KEYWORD_POST_RELEASE = 2 + +METAORDER_LOWER_BOUND = 0 +METAORDER_ZERO = 1 +METAORDER_NONZERO = 2 +METAORDER_PRE_RELEASE = 3 +METAORDER_POST_RELEASE = 4 +METAORDER_LETTER_SUFFIX = 5 +METAORDER_UPPER_BOUND = 6 + + +class LibversionVersion: + def __init__(self, version_string): + self.version_string = version_string + self.components = list(self.get_next_version_component(version_string)) + + def __hash__(self): + return hash(self.components) + + def __eq__(self, other): + return self.compare_components(other) == 0 + + def __lt__(self, other): + return self.compare_components(other) < 0 + + def __le__(self, other): + return self.compare_components(other) <= 0 + + def __gt__(self, other): + return self.compare_components(other) > 0 + + def __ge__(self, other): + return self.compare_components(other) >= 0 + + @staticmethod + def classify_keyword(s): + if s in PRE_RELEASE_KEYWORDS: + return KEYWORD_PRE_RELEASE + elif s in POST_RELEASE_KEYWORDS: + return KEYWORD_POST_RELEASE + else: + return KEYWORD_UNKNOWN + + @staticmethod + def parse_token_to_component(s): + if s.isalpha(): + start = 0 + end = len(s) + keyword_type = LibversionVersion.classify_keyword(s) + + if keyword_type == KEYWORD_PRE_RELEASE: + metaorder = METAORDER_PRE_RELEASE + elif keyword_type == KEYWORD_POST_RELEASE: + metaorder = METAORDER_POST_RELEASE + else: + metaorder = METAORDER_PRE_RELEASE + + return s, start, end, metaorder + else: + s = s.lstrip("0") + start = 0 + end = len(s) + + if start == end: + metaorder = METAORDER_ZERO + else: + metaorder = METAORDER_NONZERO + + return s, start, end, metaorder + + @staticmethod + def get_next_version_component(s): + components = re.split(r"[^a-zA-Z0-9]+", s) + + for component in components: + yield LibversionVersion.parse_token_to_component(component) + + def compare_components(self, other): + min_len = min(len(self.components), len(other.components)) + + for i in range(min_len): + c1 = self.components[i] + c2 = other.components[i] + + # Compare metaorder + if c1[3] < c2[3]: + return -1 + elif c1[3] > c2[3]: + return 1 + + # Compare if components are empty + c1_is_empty = c1[1] == c1[2] + c2_is_empty = c2[1] == c2[2] + + if c1_is_empty and c2_is_empty: + return 0 + elif c1_is_empty: + return -1 + elif c2_is_empty: + return 1 + + # Compare if components are alpha or numeric + c1_is_alpha = c1[0][c1[1]].isalpha() + c2_is_alpha = c2[0][c2[1]].isalpha() + + if c1_is_alpha and c2_is_alpha: + if c1[0][c1[1]].lower() < c2[0][c2[1]].lower(): + return -1 + elif c1[0][c1[1]].lower() > c2[0][c2[1]].lower(): + return 1 + elif c1_is_alpha: + return -1 + elif c2_is_alpha: + return 1 + + # Numeric comparison + c1_value = int(c1[0][c1[1] : c1[2]]) + c2_value = int(c2[0][c2[1] : c2[2]]) + + if c1_value < c2_value: + return -1 + elif c1_value > c2_value: + return 1 + + # All components compared are equal, check for more components + if len(self.components) < len(other.components): + return -1 + elif len(self.components) > len(other.components): + return 1 + else: + return 0 diff --git a/src/univers/versions.py b/src/univers/versions.py index 69df9d40..62ccab5d 100644 --- a/src/univers/versions.py +++ b/src/univers/versions.py @@ -13,6 +13,7 @@ from univers import gem from univers import gentoo from univers import intdot +from univers import libversion from univers import maven from univers import nuget from univers import rpm @@ -177,6 +178,16 @@ def build_value(cls, string): @classmethod def is_valid(cls, string): return intdot.IntdotVersion.is_valid(string) + + +class LibversionVersion(Version): + @classmethod + def is_valid(cls, string): + return libversion.LibversionVersion(string) + + @classmethod + def build_value(cls, string): + return libversion.LibversionVersion(string) class GenericVersion(Version): diff --git a/tests/test_version_range.py b/tests/test_version_range.py index e05a41e2..68229c16 100644 --- a/tests/test_version_range.py +++ b/tests/test_version_range.py @@ -15,14 +15,21 @@ from univers.version_range import RANGE_CLASS_BY_SCHEMES from univers.version_range import IntdotVersionRange from univers.version_range import InvalidVersionRange +from univers.version_range import LibversionVersionRange from univers.version_range import MattermostVersionRange from univers.version_range import OpensslVersionRange from univers.version_range import PypiVersionRange from univers.version_range import VersionRange from univers.version_range import build_range_from_snyk_advisory_string from univers.version_range import from_gitlab_native +<<<<<<< HEAD from univers.versions import IntdotVersion from univers.versions import LexicographicVersion +======= +from univers.versions import InvalidVersion +from univers.versions import LibversionVersion +from univers.versions import NugetVersion +>>>>>>> 1533a12 (first libversion draft) from univers.versions import OpensslVersion from univers.versions import PypiVersion from univers.versions import SemverVersion @@ -376,3 +383,10 @@ def test_version_range_lexicographic(): assert LexicographicVersion(-123) in VersionRange.from_string("vers:lexicographic/<~") assert LexicographicVersion(None) in VersionRange.from_string("vers:lexicographic/*") assert LexicographicVersion("ABC") in VersionRange.from_string("vers:lexicographic/>abc|<=None") + + +def test_version_range_libversion(): + assert LibversionVersion("1.2.3") in LibversionVersionRange.from_string("vers:libversion/*") + assert LibversionVersion("1.2.3") in LibversionVersionRange.from_string( + "vers:libversion/>0.9|<2.1.0-alpha" + ) diff --git a/tests/test_versions.py b/tests/test_versions.py index 7f7f81b8..4771fed1 100644 --- a/tests/test_versions.py +++ b/tests/test_versions.py @@ -12,8 +12,12 @@ from univers.versions import EnhancedSemanticVersion from univers.versions import GentooVersion from univers.versions import GolangVersion +<<<<<<< HEAD from univers.versions import IntdotVersion from univers.versions import LexicographicVersion +======= +from univers.versions import LibversionVersion +>>>>>>> 1533a12 (first libversion draft) from univers.versions import MavenVersion from univers.versions import NginxVersion from univers.versions import NugetVersion @@ -241,3 +245,9 @@ def test_lexicographic_version(): assert LexicographicVersion("Abc") < LexicographicVersion(None) assert LexicographicVersion("123") < LexicographicVersion("bbc") assert LexicographicVersion("2.3.4") > LexicographicVersion("1.2.3") + + +def test_libversion_version(): + assert LibversionVersion("1.2.3") == LibversionVersion("1.2.3") + assert LibversionVersion("1.2.3") > LibversionVersion("1.2.2") + assert LibversionVersion("1.2.3-alpha") > LibversionVersion("1.2.2") From 56dd5c6905ca24f18fce15ccb9e3574b0ba300e7 Mon Sep 17 00:00:00 2001 From: "Kunz, Immanuel" Date: Thu, 13 Feb 2025 09:59:05 +0100 Subject: [PATCH 2/8] fix version range tests, simplify libversion parsing Signed-off-by: Kunz, Immanuel --- src/univers/libversion.py | 50 ++++++++++++------------------------ src/univers/version_range.py | 6 +++++ 2 files changed, 22 insertions(+), 34 deletions(-) diff --git a/src/univers/libversion.py b/src/univers/libversion.py index 4605e522..761fc871 100644 --- a/src/univers/libversion.py +++ b/src/univers/libversion.py @@ -51,69 +51,51 @@ def classify_keyword(s): @staticmethod def parse_token_to_component(s): if s.isalpha(): - start = 0 - end = len(s) keyword_type = LibversionVersion.classify_keyword(s) - - if keyword_type == KEYWORD_PRE_RELEASE: - metaorder = METAORDER_PRE_RELEASE - elif keyword_type == KEYWORD_POST_RELEASE: - metaorder = METAORDER_POST_RELEASE - else: - metaorder = METAORDER_PRE_RELEASE - - return s, start, end, metaorder + metaorder = METAORDER_PRE_RELEASE if keyword_type == KEYWORD_PRE_RELEASE else METAORDER_POST_RELEASE + return s, metaorder else: s = s.lstrip("0") - start = 0 - end = len(s) - - if start == end: - metaorder = METAORDER_ZERO - else: - metaorder = METAORDER_NONZERO - - return s, start, end, metaorder + metaorder = METAORDER_ZERO if s == "" else METAORDER_NONZERO + return s, metaorder @staticmethod def get_next_version_component(s): components = re.split(r"[^a-zA-Z0-9]+", s) - for component in components: yield LibversionVersion.parse_token_to_component(component) def compare_components(self, other): min_len = min(len(self.components), len(other.components)) - for i in range(min_len): c1 = self.components[i] c2 = other.components[i] # Compare metaorder - if c1[3] < c2[3]: + if c1[1] < c2[1]: return -1 - elif c1[3] > c2[3]: + elif c1[1] > c2[1]: return 1 # Compare if components are empty - c1_is_empty = c1[1] == c1[2] - c2_is_empty = c2[1] == c2[2] + c1_is_empty = c1[0] == "" + c2_is_empty = c2[0] == "" if c1_is_empty and c2_is_empty: - return 0 + continue elif c1_is_empty: return -1 elif c2_is_empty: return 1 # Compare if components are alpha or numeric - c1_is_alpha = c1[0][c1[1]].isalpha() - c2_is_alpha = c2[0][c2[1]].isalpha() + c1_is_alpha = c1[0].isalpha() + c2_is_alpha = c2[0].isalpha() if c1_is_alpha and c2_is_alpha: - if c1[0][c1[1]].lower() < c2[0][c2[1]].lower(): + if c1[0].lower() < c2[0].lower(): return -1 - elif c1[0][c1[1]].lower() > c2[0][c2[1]].lower(): + elif c1[0].lower() > c2[0].lower(): return 1 elif c1_is_alpha: return -1 @@ -121,8 +103,8 @@ def compare_components(self, other): return 1 # Numeric comparison - c1_value = int(c1[0][c1[1] : c1[2]]) - c2_value = int(c2[0][c2[1] : c2[2]]) + c1_value = int(c1[0]) if c1[0] else 0 + c2_value = int(c2[0]) if c2[0] else 0 if c1_value < c2_value: return -1 @@ -135,4 +117,4 @@ def compare_components(self, other): elif len(self.components) > len(other.components): return 1 else: - return 0 + return 0 \ No newline at end of file diff --git a/src/univers/version_range.py b/src/univers/version_range.py index ecb68041..b977c3ff 100644 --- a/src/univers/version_range.py +++ b/src/univers/version_range.py @@ -1186,6 +1186,11 @@ def from_native(cls, string): return cls(constraints=constraints) +class LibversionVersionRange(VersionRange): + scheme = "libversion" + version_class = versions.LibversionVersion + + class MattermostVersionRange(VersionRange): scheme = "mattermost" version_class = versions.SemverVersion @@ -1446,6 +1451,7 @@ def build_range_from_snyk_advisory_string(scheme: str, string: Union[str, List]) "alpm": ArchLinuxVersionRange, "nginx": NginxVersionRange, "openssl": OpensslVersionRange, + "libversion": LibversionVersionRange, "mattermost": MattermostVersionRange, "conan": ConanVersionRange, "all": AllVersionRange, From 20611004accc8b8a4201971aae7cf2616ecbe251 Mon Sep 17 00:00:00 2001 From: "Kunz, Immanuel" Date: Wed, 19 Feb 2025 08:29:32 +0100 Subject: [PATCH 3/8] add license header, improve comments Signed-off-by: Kunz, Immanuel --- src/univers/libversion.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/univers/libversion.py b/src/univers/libversion.py index 761fc871..ecff8d31 100644 --- a/src/univers/libversion.py +++ b/src/univers/libversion.py @@ -1,3 +1,7 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download. + import re PRE_RELEASE_KEYWORDS = ["alpha", "beta", "rc", "pre"] @@ -71,13 +75,17 @@ def compare_components(self, other): c1 = self.components[i] c2 = other.components[i] - # Compare metaorder + """ + Compare based on metaorder + """ if c1[1] < c2[1]: return -1 elif c1[1] > c2[1]: return 1 - # Compare if components are empty + """ + Check based on empty components + """ c1_is_empty = c1[0] == "" c2_is_empty = c2[0] == "" @@ -88,7 +96,9 @@ def compare_components(self, other): elif c2_is_empty: return 1 - # Compare if components are alpha or numeric + """ + Compare based on alphabet or numeric + """ c1_is_alpha = c1[0].isalpha() c2_is_alpha = c2[0].isalpha() @@ -102,7 +112,9 @@ def compare_components(self, other): elif c2_is_alpha: return 1 - # Numeric comparison + """ + Compare based on numeric comparison + """ c1_value = int(c1[0]) if c1[0] else 0 c2_value = int(c2[0]) if c2[0] else 0 @@ -111,7 +123,9 @@ def compare_components(self, other): elif c1_value > c2_value: return 1 - # All components compared are equal, check for more components + """ + Components are equal; check for length of components + """ if len(self.components) < len(other.components): return -1 elif len(self.components) > len(other.components): From e214f7734195b4e6287a65635e935cada02501c4 Mon Sep 17 00:00:00 2001 From: "Kunz, Immanuel" Date: Wed, 19 Feb 2025 13:53:32 +0100 Subject: [PATCH 4/8] fix padding of empty components Signed-off-by: Kunz, Immanuel --- src/univers/libversion.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/univers/libversion.py b/src/univers/libversion.py index ecff8d31..296c5efb 100644 --- a/src/univers/libversion.py +++ b/src/univers/libversion.py @@ -70,10 +70,14 @@ def get_next_version_component(s): yield LibversionVersion.parse_token_to_component(component) def compare_components(self, other): - min_len = min(len(self.components), len(other.components)) - for i in range(min_len): - c1 = self.components[i] - c2 = other.components[i] + max_len = max(len(self.components), len(other.components)) + + for i in range(max_len): + """ + Get current components or pad with zero + """ + c1 = self.components[i] if i < len(self.components) else ("0", METAORDER_ZERO) + c2 = other.components[i] if i < len(other.components) else ("0", METAORDER_ZERO) """ Compare based on metaorder @@ -123,12 +127,4 @@ def compare_components(self, other): elif c1_value > c2_value: return 1 - """ - Components are equal; check for length of components - """ - if len(self.components) < len(other.components): - return -1 - elif len(self.components) > len(other.components): - return 1 - else: - return 0 \ No newline at end of file + return 0 \ No newline at end of file From 54e6d0889cc14fe3dbde39c5610f30e4ebd059eb Mon Sep 17 00:00:00 2001 From: "Kunz, Immanuel" Date: Wed, 19 Feb 2025 13:53:42 +0100 Subject: [PATCH 5/8] extend tests Signed-off-by: Kunz, Immanuel --- tests/test_version_range.py | 9 ++++++--- tests/test_versions.py | 11 ++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/test_version_range.py b/tests/test_version_range.py index 68229c16..b637a83b 100644 --- a/tests/test_version_range.py +++ b/tests/test_version_range.py @@ -387,6 +387,9 @@ def test_version_range_lexicographic(): def test_version_range_libversion(): assert LibversionVersion("1.2.3") in LibversionVersionRange.from_string("vers:libversion/*") - assert LibversionVersion("1.2.3") in LibversionVersionRange.from_string( - "vers:libversion/>0.9|<2.1.0-alpha" - ) + assert LibversionVersion("1.2.3") in LibversionVersionRange.from_string("vers:libversion/>0.9|<2.1.0-alpha") + assert LibversionVersion("1.0.0") in LibversionVersionRange.from_string("vers:libversion/>=1.0.0") + assert LibversionVersion("1.5.0") in LibversionVersionRange.from_string("vers:libversion/>=1.0.0|<=1.5.0") + assert not LibversionVersion("2.0.0") in LibversionVersionRange.from_string("vers:libversion/<2.0.0") + assert not LibversionVersion("1.2.3") in LibversionVersionRange.from_string("vers:libversion/>=1.2.4") + assert LibversionVersion("1.0.0") in LibversionVersionRange.from_string("vers:libversion/!=1.1.0") diff --git a/tests/test_versions.py b/tests/test_versions.py index 4771fed1..64834fbd 100644 --- a/tests/test_versions.py +++ b/tests/test_versions.py @@ -249,5 +249,14 @@ def test_lexicographic_version(): def test_libversion_version(): assert LibversionVersion("1.2.3") == LibversionVersion("1.2.3") + assert LibversionVersion("1.2.3") != LibversionVersion("1.2.4") + assert LibversionVersion.is_valid("1.2.3") + assert not LibversionVersion.is_valid("1.2.3a-1-a") + assert LibversionVersion.normalize("v1.2.3") == "1.2.3" assert LibversionVersion("1.2.3") > LibversionVersion("1.2.2") - assert LibversionVersion("1.2.3-alpha") > LibversionVersion("1.2.2") + assert LibversionVersion("1.2.3") < LibversionVersion("1.3.0") + assert LibversionVersion("1.2.3") >= LibversionVersion("1.2.3") + assert LibversionVersion("1.2.3") <= LibversionVersion("1.2.3") + assert LibversionVersion("1.2.3-alpha") < LibversionVersion("1.2.3") + assert LibversionVersion("1.2.3-alpha") != LibversionVersion("1.2.3-beta") + assert LibversionVersion("1.0") == LibversionVersion("1.0.0") From d4817f4bf92f3a6d664ba214b8503b730db380bf Mon Sep 17 00:00:00 2001 From: "Kunz, Immanuel" Date: Fri, 21 Nov 2025 12:40:33 +0100 Subject: [PATCH 6/8] resolve import problem Signed-off-by: Kunz, Immanuel --- tests/test_version_range.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_version_range.py b/tests/test_version_range.py index b637a83b..67dfea3d 100644 --- a/tests/test_version_range.py +++ b/tests/test_version_range.py @@ -22,14 +22,11 @@ from univers.version_range import VersionRange from univers.version_range import build_range_from_snyk_advisory_string from univers.version_range import from_gitlab_native -<<<<<<< HEAD from univers.versions import IntdotVersion from univers.versions import LexicographicVersion -======= from univers.versions import InvalidVersion from univers.versions import LibversionVersion from univers.versions import NugetVersion ->>>>>>> 1533a12 (first libversion draft) from univers.versions import OpensslVersion from univers.versions import PypiVersion from univers.versions import SemverVersion From f2413c4e3653a86a9f00308d0166bac7e9ef17c8 Mon Sep 17 00:00:00 2001 From: "Kunz, Immanuel" Date: Mon, 24 Nov 2025 17:09:24 +0100 Subject: [PATCH 7/8] add libversion fallback Signed-off-by: Kunz, Immanuel --- README.rst | 8 ++++- src/univers/config.py | 66 ++++++++++++++++++++++++++++++++++ src/univers/versions.py | 78 ++++++++++++++++++++++++++++++++++++++--- tests/test_versions.py | 25 +++++++++++-- 4 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 src/univers/config.py diff --git a/README.rst b/README.rst index cc44df14..30c5d2ff 100644 --- a/README.rst +++ b/README.rst @@ -120,7 +120,6 @@ and support for more package types are implemented on a continuous basis. Alternative ============ - Rather than using ecosystem-specific version schemes and code, another approach is to use a single procedure for all the versions as implemented in `libversion `_. ``libversion`` works in the most @@ -128,7 +127,14 @@ common case but may not work correctly when a task that demand precise version comparisons such as for dependency resolution and vulnerability lookup where a "good enough" comparison accuracy is not acceptable. ``libversion`` does not handle version range notations. +For this reason, univers adds support for libversion using a configuration option which allows users to use libversion as a fallback, i.e., in case the native version comparison fails. Usage: +.. code:: python + v3 = PypiVersion("1.2.3-invalid") + v4 = PypiVersion("1.2.4-invalid") + result = v3 < v4 # Error without fallback + config.use_libversion_fallback = True + result = v3 < v4 # result == True Installation ============ diff --git a/src/univers/config.py b/src/univers/config.py new file mode 100644 index 00000000..850f9d2a --- /dev/null +++ b/src/univers/config.py @@ -0,0 +1,66 @@ + +# univers/config.py + +from contextlib import contextmanager + +class Config: + """ + Global configuration for univers library. + + Simple configuration for single-threaded use. + """ + + def __init__(self): + self._use_libversion_fallback = False + + @property + def use_libversion_fallback(self): + """ + Get the current libversion fallback setting. + + Returns: + bool: True if libversion fallback is enabled, False otherwise. + """ + return self._use_libversion_fallback + + @use_libversion_fallback.setter + def use_libversion_fallback(self, value): + """ + Set the global libversion fallback setting. + + Args: + value: Boolean value to enable (True) or disable (False) fallback. + + Example: + >>> from univers import config + >>> config.use_libversion_fallback = True + """ + self._use_libversion_fallback = bool(value) + + @contextmanager + def libversion_fallback(self, enabled=True): + """ + Context manager for temporary fallback setting. + + Args: + enabled (bool): Whether to enable fallback within the context. + + Example: + >>> from univers import config + >>> from univers.versions import PypiVersion + >>> + >>> with config.libversion_fallback(enabled=True): + ... v1 = PypiVersion("1.2.3-custom") + ... v2 = PypiVersion("1.2.4-custom") + ... result = v1 < v2 + """ + old_value = self._use_libversion_fallback + self._use_libversion_fallback = enabled + try: + yield + finally: + self._use_libversion_fallback = old_value + +# Global config instance + +config = Config() \ No newline at end of file diff --git a/src/univers/versions.py b/src/univers/versions.py index 62ccab5d..44fc2195 100644 --- a/src/univers/versions.py +++ b/src/univers/versions.py @@ -9,6 +9,7 @@ from packaging import version as packaging_version from univers import arch +from univers.config import config from univers import debian from univers import gem from univers import gentoo @@ -85,17 +86,86 @@ class Version: def __attrs_post_init__(self): normalized_string = self.normalize(self.string) - if not self.is_valid(normalized_string): - raise InvalidVersion(f"{self.string!r} is not a valid {self.__class__!r}") + + # Skip validation if fallback is enabled + if not config.use_libversion_fallback: + if not self.is_valid(normalized_string): + print("Validation - config id:", id(config), "value:", config.use_libversion_fallback) + raise InvalidVersion(f"{self.string!r} is not a valid {self.__class__!r}") # Set the normalized string as default value - # Notes: setattr is used because this is an immutable frozen instance. # See https://www.attrs.org/en/stable/init.html?#post-init object.__setattr__(self, "normalized_string", normalized_string) - value = self.build_value(normalized_string) + + # Try to build value, but allow it to fail if fallback is enabled + try: + value = self.build_value(normalized_string) + except Exception as e: + if config.use_libversion_fallback: + # Store the normalized string as value if building fails + value = normalized_string + else: + raise + object.__setattr__(self, "value", value) + def __init_subclass__(cls, **kwargs): + """ + Automatically wrap comparison methods in subclasses with fallback logic. + """ + super().__init_subclass__(**kwargs) + + comparison_methods = ['__lt__', '__le__', '__gt__', '__ge__', '__eq__', '__ne__'] + + for method_name in comparison_methods: + # Only wrap if the method is defined in THIS specific class + if method_name in cls.__dict__: + original_method = cls.__dict__[method_name] + wrapped = cls._wrap_comparison_method(original_method, method_name) + setattr(cls, method_name, wrapped) + + @staticmethod + def _wrap_comparison_method(original_method, method_name): + """ + Wrap a comparison method with fallback logic. + + Uses only standard library features (no external dependencies). + """ + def wrapper(self, other): + try: + # Try the original comparison method + return original_method(self, other) + except (ValueError, TypeError, AttributeError) as e: + # If it fails and fallback is enabled, use libversion + if config.use_libversion_fallback: + try: + import libversion + result = libversion.version_compare2(str(self), str(other)) + + # Map libversion result to the appropriate comparison + if method_name == '__lt__': + return result < 0 + elif method_name == '__le__': + return result <= 0 + elif method_name == '__gt__': + return result > 0 + elif method_name == '__ge__': + return result >= 0 + elif method_name == '__eq__': + return result == 0 + elif method_name == '__ne__': + return result != 0 + except Exception: + # If fallback also fails, re-raise the original exception + raise e + # If fallback is disabled, re-raise the original exception + raise + + # Preserve method name + wrapper.__name__ = original_method.__name__ + return wrapper + @classmethod def is_valid(cls, string): """ diff --git a/tests/test_versions.py b/tests/test_versions.py index 64834fbd..b8b9486c 100644 --- a/tests/test_versions.py +++ b/tests/test_versions.py @@ -12,12 +12,9 @@ from univers.versions import EnhancedSemanticVersion from univers.versions import GentooVersion from univers.versions import GolangVersion -<<<<<<< HEAD from univers.versions import IntdotVersion from univers.versions import LexicographicVersion -======= from univers.versions import LibversionVersion ->>>>>>> 1533a12 (first libversion draft) from univers.versions import MavenVersion from univers.versions import NginxVersion from univers.versions import NugetVersion @@ -26,6 +23,7 @@ from univers.versions import RubygemsVersion from univers.versions import SemverVersion from univers.versions import Version +from univers.config import config def test_version(): @@ -260,3 +258,24 @@ def test_libversion_version(): assert LibversionVersion("1.2.3-alpha") < LibversionVersion("1.2.3") assert LibversionVersion("1.2.3-alpha") != LibversionVersion("1.2.3-beta") assert LibversionVersion("1.0") == LibversionVersion("1.0.0") + + +def test_libversion_fallback_config(): + # Default: fallback disabled + v1 = PypiVersion("1.2.3") + v2 = PypiVersion("1.2.4") + assert v1 < v2 + + # Enable globally + config.use_libversion_fallback = True + v3 = PypiVersion("1.2.3-invalid") + v4 = PypiVersion("1.2.4-invalid") + assert v3 < v4 # Uses fallback if needed + + # Temporarily enable fallback + config.use_libversion_fallback = False + with config.libversion_fallback(enabled=True): + v5 = PypiVersion("custom-1") + v6 = PypiVersion("custom-2") + assert v5 < v6 # Uses fallback if needed + From adb32463b36d7090b51184fc803f37e72c1f1121 Mon Sep 17 00:00:00 2001 From: "Kunz, Immanuel" Date: Mon, 24 Nov 2025 17:32:06 +0100 Subject: [PATCH 8/8] improve readme Signed-off-by: Kunz, Immanuel --- README.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.rst b/README.rst index 30c5d2ff..129d1d9c 100644 --- a/README.rst +++ b/README.rst @@ -128,7 +128,12 @@ comparisons such as for dependency resolution and vulnerability lookup where a "good enough" comparison accuracy is not acceptable. ``libversion`` does not handle version range notations. For this reason, univers adds support for libversion using a configuration option which allows users to use libversion as a fallback, i.e., in case the native version comparison fails. Usage: + .. code:: python + + from univers.config import config + from univers.versions import PypiVersion + v3 = PypiVersion("1.2.3-invalid") v4 = PypiVersion("1.2.4-invalid") result = v3 < v4 # Error without fallback