semver.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. # -*- coding: utf-8 -*-
  2. """
  3. Semantic Versioning (SemVer) Implementierung.
  4. Unterstützt das Parsen, Vergleichen und Validieren von
  5. semantischen Versionsnummern nach dem SemVer 2.0.0 Standard.
  6. """
  7. from __future__ import annotations
  8. import re
  9. from dataclasses import dataclass
  10. from functools import total_ordering
  11. from typing import Any
  12. # SemVer Regex Pattern
  13. SEMVER_PATTERN = re.compile(
  14. r"^(?P<major>0|[1-9]\d*)"
  15. r"\.(?P<minor>0|[1-9]\d*)"
  16. r"\.(?P<patch>0|[1-9]\d*)"
  17. r"(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)"
  18. r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
  19. r"(?:\+(?P<build>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
  20. )
  21. # Einfacheres Pattern für grundlegende Versionen
  22. SIMPLE_VERSION_PATTERN = re.compile(
  23. r"^(?P<major>\d+)(?:\.(?P<minor>\d+))?(?:\.(?P<patch>\d+))?"
  24. r"(?:-(?P<prerelease>[0-9a-zA-Z.-]+))?"
  25. r"(?:\+(?P<build>[0-9a-zA-Z.-]+))?$"
  26. )
  27. @total_ordering
  28. @dataclass
  29. class SemanticVersion:
  30. """
  31. Semantische Versionsnummer.
  32. Implementiert SemVer 2.0.0 mit major.minor.patch[-prerelease][+build].
  33. Attributes:
  34. major: Hauptversion (inkompatible API-Änderungen)
  35. minor: Nebenversion (abwärtskompatible Funktionalität)
  36. patch: Patch-Version (abwärtskompatible Bugfixes)
  37. prerelease: Pre-Release-Bezeichner (alpha, beta, rc)
  38. build: Build-Metadaten
  39. Example:
  40. v1 = SemanticVersion(1, 2, 3)
  41. v2 = parse_version("1.2.4")
  42. if v2 > v1:
  43. print("v2 ist neuer")
  44. v3 = SemanticVersion(2, 0, 0, prerelease="alpha.1")
  45. print(v3) # 2.0.0-alpha.1
  46. """
  47. major: int
  48. minor: int
  49. patch: int
  50. prerelease: str | None = None
  51. build: str | None = None
  52. def __post_init__(self) -> None:
  53. """Validiert die Version."""
  54. if self.major < 0 or self.minor < 0 or self.patch < 0:
  55. raise ValueError("Version darf keine negativen Zahlen enthalten")
  56. def __str__(self) -> str:
  57. """Gibt die Version als String zurück."""
  58. version = f"{self.major}.{self.minor}.{self.patch}"
  59. if self.prerelease:
  60. version += f"-{self.prerelease}"
  61. if self.build:
  62. version += f"+{self.build}"
  63. return version
  64. def __repr__(self) -> str:
  65. return f"SemanticVersion({self})"
  66. def __hash__(self) -> int:
  67. return hash((self.major, self.minor, self.patch, self.prerelease))
  68. def __eq__(self, other: object) -> bool:
  69. if not isinstance(other, SemanticVersion):
  70. return NotImplemented
  71. return (
  72. self.major == other.major and
  73. self.minor == other.minor and
  74. self.patch == other.patch and
  75. self.prerelease == other.prerelease
  76. )
  77. def __lt__(self, other: SemanticVersion) -> bool:
  78. if not isinstance(other, SemanticVersion):
  79. return NotImplemented
  80. # Major, Minor, Patch vergleichen
  81. if (self.major, self.minor, self.patch) != (other.major, other.minor, other.patch):
  82. return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
  83. # Pre-release Vergleich
  84. # Eine Version ohne Pre-release ist größer als eine mit Pre-release
  85. if self.prerelease is None and other.prerelease is None:
  86. return False
  87. if self.prerelease is None:
  88. return False # Keine Pre-release > Pre-release
  89. if other.prerelease is None:
  90. return True # Pre-release < Keine Pre-release
  91. # Pre-release-Teile vergleichen
  92. return self._compare_prerelease(self.prerelease, other.prerelease) < 0
  93. @staticmethod
  94. def _compare_prerelease(a: str, b: str) -> int:
  95. """
  96. Vergleicht zwei Pre-release-Strings.
  97. Returns:
  98. -1 wenn a < b, 0 wenn gleich, 1 wenn a > b
  99. """
  100. parts_a = a.split(".")
  101. parts_b = b.split(".")
  102. for part_a, part_b in zip(parts_a, parts_b):
  103. # Numerische Teile numerisch vergleichen
  104. is_num_a = part_a.isdigit()
  105. is_num_b = part_b.isdigit()
  106. if is_num_a and is_num_b:
  107. num_a, num_b = int(part_a), int(part_b)
  108. if num_a != num_b:
  109. return -1 if num_a < num_b else 1
  110. elif is_num_a:
  111. return -1 # Numerisch < String
  112. elif is_num_b:
  113. return 1 # String > Numerisch
  114. else:
  115. if part_a != part_b:
  116. return -1 if part_a < part_b else 1
  117. # Längerer Pre-release hat höhere Priorität
  118. if len(parts_a) != len(parts_b):
  119. return -1 if len(parts_a) < len(parts_b) else 1
  120. return 0
  121. @property
  122. def is_prerelease(self) -> bool:
  123. """Ist dies eine Pre-Release-Version?"""
  124. return self.prerelease is not None
  125. @property
  126. def is_stable(self) -> bool:
  127. """Ist dies eine stabile Version (>= 1.0.0)?"""
  128. return self.major >= 1 and not self.is_prerelease
  129. def bump_major(self) -> SemanticVersion:
  130. """Gibt eine neue Version mit erhöhter Major-Version zurück."""
  131. return SemanticVersion(self.major + 1, 0, 0)
  132. def bump_minor(self) -> SemanticVersion:
  133. """Gibt eine neue Version mit erhöhter Minor-Version zurück."""
  134. return SemanticVersion(self.major, self.minor + 1, 0)
  135. def bump_patch(self) -> SemanticVersion:
  136. """Gibt eine neue Version mit erhöhter Patch-Version zurück."""
  137. return SemanticVersion(self.major, self.minor, self.patch + 1)
  138. def to_tuple(self) -> tuple[int, int, int]:
  139. """Gibt die Version als Tuple zurück."""
  140. return (self.major, self.minor, self.patch)
  141. def to_dict(self) -> dict[str, Any]:
  142. """Gibt die Version als Dictionary zurück."""
  143. return {
  144. "major": self.major,
  145. "minor": self.minor,
  146. "patch": self.patch,
  147. "prerelease": self.prerelease,
  148. "build": self.build,
  149. }
  150. class VersionRange:
  151. """
  152. Versionsbereich für Abhängigkeitsspezifikationen.
  153. Unterstützt verschiedene Operatoren:
  154. - Exakt: "1.2.3" oder "=1.2.3"
  155. - Größer: ">1.0.0", ">=1.0.0"
  156. - Kleiner: "<2.0.0", "<=2.0.0"
  157. - Bereich: ">=1.0.0 <2.0.0"
  158. - Caret: "^1.2.3" (kompatibel mit 1.x.x)
  159. - Tilde: "~1.2.3" (kompatibel mit 1.2.x)
  160. - Wildcard: "1.2.*", "1.*"
  161. Example:
  162. range1 = VersionRange("^1.2.0")
  163. assert range1.satisfies(parse_version("1.5.0"))
  164. assert not range1.satisfies(parse_version("2.0.0"))
  165. range2 = VersionRange(">=1.0.0 <2.0.0")
  166. assert range2.satisfies(parse_version("1.9.9"))
  167. """
  168. OPERATORS = [">=", "<=", "!=", ">", "<", "=", "^", "~"]
  169. def __init__(self, spec: str) -> None:
  170. """
  171. Initialisiert den Versionsbereich.
  172. Args:
  173. spec: Versionsspezifikation
  174. """
  175. self._spec = spec.strip()
  176. self._constraints = self._parse_spec(self._spec)
  177. def _parse_spec(self, spec: str) -> list[tuple[str, SemanticVersion]]:
  178. """Parst die Spezifikation in Constraints."""
  179. constraints = []
  180. # Wildcard-Handling
  181. if "*" in spec:
  182. return self._parse_wildcard(spec)
  183. # Mehrere Constraints (durch Leerzeichen oder Komma getrennt)
  184. parts = re.split(r"[,\s]+", spec)
  185. for part in parts:
  186. part = part.strip()
  187. if not part:
  188. continue
  189. # Operator extrahieren
  190. operator = "="
  191. version_str = part
  192. for op in sorted(self.OPERATORS, key=len, reverse=True):
  193. if part.startswith(op):
  194. operator = op
  195. version_str = part[len(op):].strip()
  196. break
  197. # Caret und Tilde zu Range konvertieren
  198. if operator == "^":
  199. constraints.extend(self._expand_caret(version_str))
  200. elif operator == "~":
  201. constraints.extend(self._expand_tilde(version_str))
  202. else:
  203. version = parse_version(version_str)
  204. constraints.append((operator, version))
  205. return constraints
  206. def _parse_wildcard(self, spec: str) -> list[tuple[str, SemanticVersion]]:
  207. """Parst Wildcard-Spezifikationen."""
  208. parts = spec.replace("*", "0").split(".")
  209. if len(parts) == 1:
  210. # "*" -> Alles
  211. return []
  212. major = int(parts[0]) if parts[0] != "0" or spec.startswith("0") else 0
  213. if len(parts) == 2 or (len(parts) >= 2 and parts[1] == "0" and "*" in spec):
  214. # "1.*" -> >=1.0.0 <2.0.0
  215. return [
  216. (">=", SemanticVersion(major, 0, 0)),
  217. ("<", SemanticVersion(major + 1, 0, 0)),
  218. ]
  219. minor = int(parts[1])
  220. # "1.2.*" -> >=1.2.0 <1.3.0
  221. return [
  222. (">=", SemanticVersion(major, minor, 0)),
  223. ("<", SemanticVersion(major, minor + 1, 0)),
  224. ]
  225. def _expand_caret(self, version_str: str) -> list[tuple[str, SemanticVersion]]:
  226. """Expandiert Caret-Range (^1.2.3 -> >=1.2.3 <2.0.0)."""
  227. version = parse_version(version_str)
  228. if version.major == 0:
  229. if version.minor == 0:
  230. # ^0.0.x -> =0.0.x
  231. upper = SemanticVersion(0, 0, version.patch + 1)
  232. else:
  233. # ^0.x.y -> >=0.x.y <0.(x+1).0
  234. upper = SemanticVersion(0, version.minor + 1, 0)
  235. else:
  236. # ^x.y.z -> >=x.y.z <(x+1).0.0
  237. upper = SemanticVersion(version.major + 1, 0, 0)
  238. return [(">=", version), ("<", upper)]
  239. def _expand_tilde(self, version_str: str) -> list[tuple[str, SemanticVersion]]:
  240. """Expandiert Tilde-Range (~1.2.3 -> >=1.2.3 <1.3.0)."""
  241. version = parse_version(version_str)
  242. upper = SemanticVersion(version.major, version.minor + 1, 0)
  243. return [(">=", version), ("<", upper)]
  244. def satisfies(self, version: SemanticVersion) -> bool:
  245. """
  246. Prüft, ob eine Version den Bereich erfüllt.
  247. Args:
  248. version: Zu prüfende Version
  249. Returns:
  250. True wenn die Version im Bereich liegt
  251. """
  252. if not self._constraints:
  253. return True # Leerer Bereich = Alles
  254. for operator, constraint in self._constraints:
  255. if not self._check_constraint(version, operator, constraint):
  256. return False
  257. return True
  258. def _check_constraint(
  259. self,
  260. version: SemanticVersion,
  261. operator: str,
  262. constraint: SemanticVersion
  263. ) -> bool:
  264. """Prüft einen einzelnen Constraint."""
  265. if operator == "=":
  266. return version == constraint
  267. elif operator == "!=":
  268. return version != constraint
  269. elif operator == ">":
  270. return version > constraint
  271. elif operator == ">=":
  272. return version >= constraint
  273. elif operator == "<":
  274. return version < constraint
  275. elif operator == "<=":
  276. return version <= constraint
  277. return False
  278. @property
  279. def spec(self) -> str:
  280. """Gibt die ursprüngliche Spezifikation zurück."""
  281. return self._spec
  282. def __str__(self) -> str:
  283. return self._spec
  284. def __repr__(self) -> str:
  285. return f"VersionRange({self._spec!r})"
  286. def __contains__(self, version: SemanticVersion) -> bool:
  287. return self.satisfies(version)
  288. def parse_version(version_str: str) -> SemanticVersion:
  289. """
  290. Parst einen Versions-String zu einer SemanticVersion.
  291. Unterstützt vollständige und vereinfachte Versionen:
  292. - "1.2.3"
  293. - "1.2.3-alpha.1"
  294. - "1.2.3+build.123"
  295. - "1.2" -> "1.2.0"
  296. - "1" -> "1.0.0"
  297. Args:
  298. version_str: Versions-String
  299. Returns:
  300. SemanticVersion-Objekt
  301. Raises:
  302. ValueError: Bei ungültigem Format
  303. """
  304. version_str = version_str.strip()
  305. if version_str.startswith("v") or version_str.startswith("V"):
  306. version_str = version_str[1:]
  307. # Erst vollständiges SemVer probieren
  308. match = SEMVER_PATTERN.match(version_str)
  309. if not match:
  310. # Einfacheres Pattern probieren
  311. match = SIMPLE_VERSION_PATTERN.match(version_str)
  312. if not match:
  313. raise ValueError(f"Ungültiges Versionsformat: {version_str}")
  314. groups = match.groupdict()
  315. major = int(groups["major"])
  316. minor = int(groups.get("minor") or 0)
  317. patch = int(groups.get("patch") or 0)
  318. prerelease = groups.get("prerelease")
  319. build = groups.get("build")
  320. return SemanticVersion(major, minor, patch, prerelease, build)
  321. def parse_range(spec: str) -> VersionRange:
  322. """
  323. Parst einen Range-String zu einem VersionRange.
  324. Args:
  325. spec: Range-Spezifikation
  326. Returns:
  327. VersionRange-Objekt
  328. """
  329. return VersionRange(spec)