Compare Semantic Versions Without Dependencies
Parse and compare semantic version strings with prerelease precedence, then sort a mixed release list correctly.
Code
from __future__ import annotations
import re
from dataclasses import dataclass
from functools import total_ordering
SEMVER_RE = re.compile(
r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)"
r"(?:-(?P<prerelease>[0-9A-Za-z.-]+))?"
r"(?:\+(?P<build>[0-9A-Za-z.-]+))?$"
)
@total_ordering
@dataclass(frozen=True)
class Version:
major: int
minor: int
patch: int
prerelease: tuple[str, ...] = ()
@classmethod
def parse(cls, text: str) -> Version:
match = SEMVER_RE.match(text.strip())
if match is None:
raise ValueError(f"invalid semantic version: {text!r}")
prerelease = match.group("prerelease")
return cls(
major=int(match.group("major")),
minor=int(match.group("minor")),
patch=int(match.group("patch")),
prerelease=tuple(prerelease.split(".")) if prerelease else (),
)
def _rank(self) -> tuple[object, ...]:
if not self.prerelease:
return (1,)
identifiers = tuple(
(0, int(part)) if part.isdigit() else (1, part)
for part in self.prerelease
)
return (0, identifiers)
def __lt__(self, other: Version) -> bool:
return (self.major, self.minor, self.patch, self._rank()) < (
other.major,
other.minor,
other.patch,
other._rank(),
)
def __str__(self) -> str:
base = f"{self.major}.{self.minor}.{self.patch}"
if self.prerelease:
return f"{base}-{'.'.join(self.prerelease)}"
return base
def main() -> None:
raw = [
"1.0.0",
"1.0.0-alpha",
"1.0.0-alpha.1",
"1.0.0-beta",
"1.0.0-rc.1",
"1.10.0",
"2.0.0",
]
ordered = [str(version) for version in sorted(Version.parse(item) for item in raw)]
print(" < ".join(ordered))
assert ordered == [
"1.0.0-alpha",
"1.0.0-alpha.1",
"1.0.0-beta",
"1.0.0-rc.1",
"1.0.0",
"1.10.0",
"2.0.0",
]
assert Version.parse("1.0.0+build.9") == Version.parse("1.0.0")
try:
Version.parse("1.0")
except ValueError as exc:
print(f"rejected: {exc}")
if __name__ == "__main__":
main()
Report a problem with this exampleOpens a pre-filled issue on GitHub.