Skip to content

Commit 855179a

Browse files
authored
Version 0.4.9 (#15)
1 parent 4a16fc9 commit 855179a

5 files changed

Lines changed: 104 additions & 60 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.4.9] 2026-02-06
6+
### Fixed
7+
- `Version` now ignores buildmetadata when comparing versions.
8+
59
## [0.4.8] 2026-02-06
610
### Added
711
- `checksum_any` now supports `Enum` instances.

CITATION.cff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ keywords:
1717
- tools
1818
- utilities
1919
license: MIT
20-
version: 0.4.8
20+
version: 0.4.9
2121
date-released: '2026-02-06'

src/pythonwrench/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
__license__ = "MIT"
1010
__maintainer__ = "Étienne Labbé (Labbeti)"
1111
__status__ = "Development"
12-
__version__ = "0.4.8"
12+
__version__ = "0.4.9"
1313

1414

1515
# Re-import for language servers

src/pythonwrench/semver.py

Lines changed: 68 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import re
66
import sys
77
from dataclasses import asdict, dataclass
8-
from typing import Any, Iterable, List, Mapping, Tuple, TypedDict, Union, overload
8+
from typing import Any, List, Mapping, Tuple, TypedDict, Union, overload
99

1010
from typing_extensions import NotRequired, Self, TypeAlias
1111

@@ -40,7 +40,7 @@ class VersionDict(TypedDict):
4040
]
4141

4242
VersionDictLike: TypeAlias = Mapping[str, Union[int, PreRelease, BuildMetadata]]
43-
VersionTupleLike: TypeAlias = Iterable[Union[int, PreRelease, BuildMetadata]]
43+
VersionTupleLike: TypeAlias = Tuple[Union[int, PreRelease, BuildMetadata], ...]
4444
VersionLike: TypeAlias = Union["Version", str, VersionDictLike, VersionTupleLike]
4545

4646

@@ -50,7 +50,7 @@ class Version:
5050
5151
Version format is: MAJOR.MINOR.PATCH[-PRERELEASE][+BUILDMETADATA]
5252
53-
Based on https://semver.org/.
53+
Based on https://semver.org/ version 2.0.0.
5454
"""
5555

5656
major: int
@@ -266,11 +266,8 @@ def to_tuple(
266266
version_tuple = tuple(self.to_dict(exclude_none).values())
267267
return version_tuple # type: ignore
268268

269-
def __str__(self) -> str:
270-
return self.to_str()
271-
272-
def __eq__(self, other: Any) -> bool:
273-
if isinstance(other, (dict, tuple, str)):
269+
def equals(self, other: VersionLike, *, ignore_buildmetadata: bool = False) -> bool:
270+
if isinstance(other, (Mapping, tuple, str)):
274271
other = Version(other)
275272
# note: use self.__class__ to avoid error cause by 'pytest -v test' collect
276273
elif not isinstance(other, (Version, self.__class__)):
@@ -281,67 +278,80 @@ def __eq__(self, other: Any) -> bool:
281278
and self.minor == other.minor
282279
and self.patch == other.patch
283280
and self.prerelease == other.prerelease
284-
and self.buildmetadata == other.buildmetadata
281+
and (ignore_buildmetadata or self.buildmetadata == other.buildmetadata)
285282
)
286283

287-
def __lt__(self, other: VersionLike) -> bool:
288-
if isinstance(other, (dict, tuple, str)):
289-
other = Version(other)
290-
# note: use self.__class__ to avoid error cause by 'pytest -v test' collect
291-
elif not isinstance(other, (Version, self.__class__)):
292-
msg = f"Invalid argument type {type(other)}. (expected an instance of one of {(dict, tuple, str, Version)})"
293-
raise TypeError(msg)
294-
295-
self_tuple = self.to_tuple(exclude_none=False)
296-
other_tuple = other.to_tuple(exclude_none=False)
297-
298-
for self_v, other_v in zip(self_tuple, other_tuple):
299-
if self_v == other_v:
300-
continue
301-
if self_v is None and other_v is not None:
302-
return False
303-
if self_v is not None and other_v is None:
304-
return True
284+
def __str__(self) -> str:
285+
return self.to_str()
305286

306-
if isinstance(self_v, (int, str, NoneType)):
307-
self_v = [self_v]
308-
elif not isinstance(self_v, list):
309-
raise TypeError(f"Invalid argument type {type(self_v)}.")
310-
311-
if isinstance(other_v, (int, str, NoneType)):
312-
other_v = [other_v]
313-
elif not isinstance(other_v, list):
314-
raise TypeError(f"Invalid argument type {type(other_v)}.")
315-
316-
minlen = min(len(self_v), len(other_v))
317-
if len(self_v) != len(other_v) and self_v[:minlen] == other_v[:minlen]:
318-
return len(self_v) < len(other_v)
319-
320-
for self_vi, other_vi in zip(self_v, other_v):
321-
if self_vi == other_vi:
322-
continue
323-
if isinstance(self_vi, int) and isinstance(other_vi, int):
324-
return self_vi < other_vi
325-
if isinstance(self_vi, int) and isinstance(other_vi, str):
326-
return True
327-
if isinstance(self_vi, str) and isinstance(other_vi, int):
328-
return False
329-
if isinstance(self_vi, str) and isinstance(other_vi, str):
330-
return self_vi < other_vi
331-
332-
msg = f"Invalid attribute type {self_vi=} and {other_vi=}."
333-
raise TypeError(msg)
287+
def __eq__(self, other: Any) -> bool:
288+
return self.equals(other)
334289

335-
return False
290+
def __lt__(self, other: VersionLike) -> bool:
291+
return _compare_lt(self, other)
336292

337293
def __le__(self, other: VersionLike) -> bool:
338294
return (self == other) or (self < other)
339295

340296
def __gt__(self, other: VersionLike) -> bool:
341-
return (self != other) and not (self < other)
297+
return _compare_lt(other, self)
342298

343299
def __ge__(self, other: VersionLike) -> bool:
344-
return not (self < other)
300+
return (self == other) or (self > other)
301+
302+
303+
def _compare_lt(
304+
x: Union[Version, Mapping, tuple, str], y: Union[Version, Mapping, tuple, str]
305+
) -> bool:
306+
if isinstance(x, (Mapping, tuple, str)):
307+
x = Version(x)
308+
if isinstance(y, (Mapping, tuple, str)):
309+
y = Version(y)
310+
311+
self_tuple = x.to_tuple(exclude_none=False)
312+
other_tuple = y.to_tuple(exclude_none=False)
313+
314+
self_tuple = self_tuple[:4]
315+
other_tuple = other_tuple[:4]
316+
317+
for self_v, other_v in zip(self_tuple, other_tuple):
318+
if self_v == other_v:
319+
continue
320+
if self_v is None and other_v is not None:
321+
return False
322+
if self_v is not None and other_v is None:
323+
return True
324+
325+
if isinstance(self_v, (int, str, NoneType)):
326+
self_v = [self_v]
327+
elif not isinstance(self_v, list):
328+
raise TypeError(f"Invalid argument type {type(self_v)}.")
329+
330+
if isinstance(other_v, (int, str, NoneType)):
331+
other_v = [other_v]
332+
elif not isinstance(other_v, list):
333+
raise TypeError(f"Invalid argument type {type(other_v)}.")
334+
335+
minlen = min(len(self_v), len(other_v))
336+
if len(self_v) != len(other_v) and self_v[:minlen] == other_v[:minlen]:
337+
return len(self_v) < len(other_v)
338+
339+
for self_vi, other_vi in zip(self_v, other_v):
340+
if self_vi == other_vi:
341+
continue
342+
if isinstance(self_vi, int) and isinstance(other_vi, int):
343+
return self_vi < other_vi
344+
if isinstance(self_vi, int) and isinstance(other_vi, str):
345+
return True
346+
if isinstance(self_vi, str) and isinstance(other_vi, int):
347+
return False
348+
if isinstance(self_vi, str) and isinstance(other_vi, str):
349+
return self_vi < other_vi
350+
351+
msg = f"Invalid attribute type {self_vi=} and {other_vi=}."
352+
raise TypeError(msg)
353+
354+
return False
345355

346356

347357
def _parse_version_str(version_str: str) -> VersionDict:

tests/test_semver.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ def test_versions(self) -> None:
6363
# Check if versions can be parsed
6464
Version(pw.__version__)
6565

66+
# buildmetadata can contains "-" symbol
67+
v14 = Version("1.0.0+build-info")
68+
assert v14.to_dict() == {
69+
"major": 1,
70+
"minor": 0,
71+
"patch": 0,
72+
"buildmetadata": "build-info",
73+
}
74+
6675
def test_parse_invalid(self) -> None:
6776
with self.assertRaises(ValueError):
6877
Version() # type: ignore
@@ -133,6 +142,27 @@ def test_parse(self) -> None:
133142
for version_str in tests:
134143
_ = Version(version_str)
135144

145+
def test_priority(self) -> None:
146+
v1 = Version("1.0.0")
147+
v2 = Version("1.0.0-alpha")
148+
v3 = Version("1.0.0+build")
149+
v4 = Version("1.0.0-alpha+build")
150+
151+
# IMPORTANT: prerelease should have lower precedence than the associated normal version
152+
assert v1 > v2
153+
assert not (v1 < v2)
154+
assert v1 != v2
155+
156+
# IMPORTANT: build metadata should not affect version precedence
157+
assert not (v1 > v3)
158+
assert not (v1 < v3)
159+
assert v1 != v3
160+
161+
# check both
162+
assert v1 > v4
163+
assert not (v1 < v4)
164+
assert v1 != v4
165+
136166

137167
if __name__ == "__main__":
138168
unittest.main()

0 commit comments

Comments
 (0)