Skip to content

Commit 540b401

Browse files
committed
remove config option, correct libversion algorithm
1 parent adb3246 commit 540b401

File tree

5 files changed

+81
-194
lines changed

5 files changed

+81
-194
lines changed

README.rst

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,19 +127,16 @@ common case but may not work correctly when a task that demand precise version
127127
comparisons such as for dependency resolution and vulnerability lookup where
128128
a "good enough" comparison accuracy is not acceptable. ``libversion`` does not
129129
handle version range notations.
130-
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:
130+
For this reason, univers provides a dedicated libversion scheme for users who
131+
explicitly choose that behavior. Usage:
131132

132133
.. code:: python
133134
134-
from univers.config import config
135-
from univers.versions import PypiVersion
136-
137-
v3 = PypiVersion("1.2.3-invalid")
138-
v4 = PypiVersion("1.2.4-invalid")
139-
result = v3 < v4 # Error without fallback
135+
from univers.versions import LibversionVersion
140136
141-
config.use_libversion_fallback = True
142-
result = v3 < v4 # result == True
137+
v3 = LibversionVersion("1.2.3-invalid")
138+
v4 = LibversionVersion("1.2.4-invalid")
139+
result = v3 < v4
143140
144141
Installation
145142
============

src/univers/config.py

Lines changed: 0 additions & 66 deletions
This file was deleted.

src/univers/libversion.py

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
KEYWORD_POST_RELEASE = 2
1313

1414
METAORDER_LOWER_BOUND = 0
15-
METAORDER_ZERO = 1
16-
METAORDER_NONZERO = 2
17-
METAORDER_PRE_RELEASE = 3
18-
METAORDER_POST_RELEASE = 4
15+
METAORDER_PRE_RELEASE = 1
16+
METAORDER_ZERO = 2
17+
METAORDER_POST_RELEASE = 3
18+
METAORDER_NONZERO = 4
1919
METAORDER_LETTER_SUFFIX = 5
2020
METAORDER_UPPER_BOUND = 6
2121

22+
_TOKEN_RE = re.compile(r"[A-Za-z]+|[0-9]+")
23+
2224

2325
class LibversionVersion:
2426
def __init__(self, version_string):
@@ -52,22 +54,61 @@ def classify_keyword(s):
5254
else:
5355
return KEYWORD_UNKNOWN
5456

55-
@staticmethod
56-
def parse_token_to_component(s):
57-
if s.isalpha():
58-
keyword_type = LibversionVersion.classify_keyword(s)
59-
metaorder = METAORDER_PRE_RELEASE if keyword_type == KEYWORD_PRE_RELEASE else METAORDER_POST_RELEASE
60-
return s, metaorder
61-
else:
62-
s = s.lstrip("0")
63-
metaorder = METAORDER_ZERO if s == "" else METAORDER_NONZERO
64-
return s, metaorder
65-
6657
@staticmethod
6758
def get_next_version_component(s):
68-
components = re.split(r"[^a-zA-Z0-9]+", s)
69-
for component in components:
70-
yield LibversionVersion.parse_token_to_component(component)
59+
tokens = list(_TOKEN_RE.finditer(s))
60+
token_count = len(tokens)
61+
parsed_tokens = []
62+
63+
prev_end = None
64+
for token in tokens:
65+
start, end = token.span()
66+
delim_before = prev_end is not None and start > prev_end
67+
parsed_tokens.append(
68+
{
69+
"value": token.group(0),
70+
"is_alpha": token.group(0).isalpha(),
71+
"delim_before": delim_before,
72+
"start": start,
73+
"end": end,
74+
}
75+
)
76+
prev_end = end
77+
78+
for i, token in enumerate(parsed_tokens):
79+
next_token = parsed_tokens[i + 1] if i + 1 < token_count else None
80+
delim_after = next_token is not None and next_token["start"] > token["end"]
81+
82+
if token["is_alpha"]:
83+
raw_value = token["value"]
84+
value = raw_value.lower()
85+
keyword_type = LibversionVersion.classify_keyword(value)
86+
87+
is_letter_suffix = False
88+
if i > 0 and not token["delim_before"]:
89+
prev_token = parsed_tokens[i - 1]
90+
if not prev_token["is_alpha"]:
91+
next_is_numeric_no_delim = (
92+
next_token is not None
93+
and not next_token["is_alpha"]
94+
and not delim_after
95+
)
96+
if not next_is_numeric_no_delim:
97+
is_letter_suffix = True
98+
99+
if is_letter_suffix:
100+
metaorder = METAORDER_LETTER_SUFFIX
101+
elif keyword_type == KEYWORD_POST_RELEASE:
102+
metaorder = METAORDER_POST_RELEASE
103+
else:
104+
metaorder = METAORDER_PRE_RELEASE
105+
106+
yield value, metaorder
107+
continue
108+
109+
value = token["value"].lstrip("0")
110+
metaorder = METAORDER_ZERO if value == "" else METAORDER_NONZERO
111+
yield value, metaorder
71112

72113
def compare_components(self, other):
73114
max_len = max(len(self.components), len(other.components))
@@ -76,8 +117,8 @@ def compare_components(self, other):
76117
"""
77118
Get current components or pad with zero
78119
"""
79-
c1 = self.components[i] if i < len(self.components) else ("0", METAORDER_ZERO)
80-
c2 = other.components[i] if i < len(other.components) else ("0", METAORDER_ZERO)
120+
c1 = self.components[i] if i < len(self.components) else ("", METAORDER_ZERO)
121+
c2 = other.components[i] if i < len(other.components) else ("", METAORDER_ZERO)
81122

82123
"""
83124
Compare based on metaorder
@@ -107,10 +148,13 @@ def compare_components(self, other):
107148
c2_is_alpha = c2[0].isalpha()
108149

109150
if c1_is_alpha and c2_is_alpha:
110-
if c1[0].lower() < c2[0].lower():
151+
c1_letter = c1[0][0].lower() if c1[0] else ""
152+
c2_letter = c2[0][0].lower() if c2[0] else ""
153+
if c1_letter < c2_letter:
111154
return -1
112-
elif c1[0].lower() > c2[0].lower():
155+
elif c1_letter > c2_letter:
113156
return 1
157+
continue
114158
elif c1_is_alpha:
115159
return -1
116160
elif c2_is_alpha:

src/univers/versions.py

Lines changed: 4 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from packaging import version as packaging_version
1010

1111
from univers import arch
12-
from univers.config import config
1312
from univers import debian
1413
from univers import gem
1514
from univers import gentoo
@@ -86,85 +85,16 @@ class Version:
8685

8786
def __attrs_post_init__(self):
8887
normalized_string = self.normalize(self.string)
89-
90-
# Skip validation if fallback is enabled
91-
if not config.use_libversion_fallback:
92-
if not self.is_valid(normalized_string):
93-
print("Validation - config id:", id(config), "value:", config.use_libversion_fallback)
94-
raise InvalidVersion(f"{self.string!r} is not a valid {self.__class__!r}")
88+
if not self.is_valid(normalized_string):
89+
raise InvalidVersion(f"{self.string!r} is not a valid {self.__class__!r}")
9590

9691
# Set the normalized string as default value
9792
# Notes: setattr is used because this is an immutable frozen instance.
9893
# See https://www.attrs.org/en/stable/init.html?#post-init
9994
object.__setattr__(self, "normalized_string", normalized_string)
100-
101-
# Try to build value, but allow it to fail if fallback is enabled
102-
try:
103-
value = self.build_value(normalized_string)
104-
except Exception as e:
105-
if config.use_libversion_fallback:
106-
# Store the normalized string as value if building fails
107-
value = normalized_string
108-
else:
109-
raise
110-
111-
object.__setattr__(self, "value", value)
11295

113-
def __init_subclass__(cls, **kwargs):
114-
"""
115-
Automatically wrap comparison methods in subclasses with fallback logic.
116-
"""
117-
super().__init_subclass__(**kwargs)
118-
119-
comparison_methods = ['__lt__', '__le__', '__gt__', '__ge__', '__eq__', '__ne__']
120-
121-
for method_name in comparison_methods:
122-
# Only wrap if the method is defined in THIS specific class
123-
if method_name in cls.__dict__:
124-
original_method = cls.__dict__[method_name]
125-
wrapped = cls._wrap_comparison_method(original_method, method_name)
126-
setattr(cls, method_name, wrapped)
127-
128-
@staticmethod
129-
def _wrap_comparison_method(original_method, method_name):
130-
"""
131-
Wrap a comparison method with fallback logic.
132-
133-
Uses only standard library features (no external dependencies).
134-
"""
135-
def wrapper(self, other):
136-
try:
137-
# Try the original comparison method
138-
return original_method(self, other)
139-
except (ValueError, TypeError, AttributeError) as e:
140-
# If it fails and fallback is enabled, use libversion
141-
if config.use_libversion_fallback:
142-
try:
143-
import libversion
144-
result = libversion.version_compare2(str(self), str(other))
145-
146-
# Map libversion result to the appropriate comparison
147-
if method_name == '__lt__':
148-
return result < 0
149-
elif method_name == '__le__':
150-
return result <= 0
151-
elif method_name == '__gt__':
152-
return result > 0
153-
elif method_name == '__ge__':
154-
return result >= 0
155-
elif method_name == '__eq__':
156-
return result == 0
157-
elif method_name == '__ne__':
158-
return result != 0
159-
except Exception:
160-
# If fallback also fails, re-raise the original exception
161-
raise e
162-
# If fallback is disabled, re-raise the original exception
163-
raise
164-
165-
# Preserve method name
166-
wrapper.__name__ = original_method.__name__
167-
return wrapper
96+
value = self.build_value(normalized_string)
97+
object.__setattr__(self, "value", value)
16898

16999
@classmethod
170100
def is_valid(cls, string):

tests/test_versions.py

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
from univers.versions import RubygemsVersion
2424
from univers.versions import SemverVersion
2525
from univers.versions import Version
26-
from univers.config import config
2726

2827

2928
def test_version():
@@ -249,33 +248,16 @@ def test_libversion_version():
249248
assert LibversionVersion("1.2.3") == LibversionVersion("1.2.3")
250249
assert LibversionVersion("1.2.3") != LibversionVersion("1.2.4")
251250
assert LibversionVersion.is_valid("1.2.3")
252-
assert not LibversionVersion.is_valid("1.2.3a-1-a")
253251
assert LibversionVersion.normalize("v1.2.3") == "1.2.3"
254252
assert LibversionVersion("1.2.3") > LibversionVersion("1.2.2")
255253
assert LibversionVersion("1.2.3") < LibversionVersion("1.3.0")
256254
assert LibversionVersion("1.2.3") >= LibversionVersion("1.2.3")
257255
assert LibversionVersion("1.2.3") <= LibversionVersion("1.2.3")
258256
assert LibversionVersion("1.2.3-alpha") < LibversionVersion("1.2.3")
259257
assert LibversionVersion("1.2.3-alpha") != LibversionVersion("1.2.3-beta")
258+
assert LibversionVersion("1.0custom1") < LibversionVersion("1.0")
259+
assert LibversionVersion("1.0alpha1") == LibversionVersion("1.0a1")
260+
assert LibversionVersion("1.0") < LibversionVersion("1.0a")
261+
assert LibversionVersion("1.0.1") < LibversionVersion("1.0a")
262+
assert LibversionVersion("1.0a1") < LibversionVersion("1.0")
260263
assert LibversionVersion("1.0") == LibversionVersion("1.0.0")
261-
262-
263-
def test_libversion_fallback_config():
264-
# Default: fallback disabled
265-
v1 = PypiVersion("1.2.3")
266-
v2 = PypiVersion("1.2.4")
267-
assert v1 < v2
268-
269-
# Enable globally
270-
config.use_libversion_fallback = True
271-
v3 = PypiVersion("1.2.3-invalid")
272-
v4 = PypiVersion("1.2.4-invalid")
273-
assert v3 < v4 # Uses fallback if needed
274-
275-
# Temporarily enable fallback
276-
config.use_libversion_fallback = False
277-
with config.libversion_fallback(enabled=True):
278-
v5 = PypiVersion("custom-1")
279-
v6 = PypiVersion("custom-2")
280-
assert v5 < v6 # Uses fallback if needed
281-

0 commit comments

Comments
 (0)