| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- # -*- coding: utf-8 -*-
- """
- Semantic Versioning (SemVer) Implementierung.
- Unterstützt das Parsen, Vergleichen und Validieren von
- semantischen Versionsnummern nach dem SemVer 2.0.0 Standard.
- """
- from __future__ import annotations
- import re
- from dataclasses import dataclass
- from functools import total_ordering
- from typing import Any
- # SemVer Regex Pattern
- SEMVER_PATTERN = re.compile(
- r"^(?P<major>0|[1-9]\d*)"
- r"\.(?P<minor>0|[1-9]\d*)"
- r"\.(?P<patch>0|[1-9]\d*)"
- r"(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)"
- r"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
- r"(?:\+(?P<build>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
- )
- # Einfacheres Pattern für grundlegende Versionen
- SIMPLE_VERSION_PATTERN = re.compile(
- r"^(?P<major>\d+)(?:\.(?P<minor>\d+))?(?:\.(?P<patch>\d+))?"
- r"(?:-(?P<prerelease>[0-9a-zA-Z.-]+))?"
- r"(?:\+(?P<build>[0-9a-zA-Z.-]+))?$"
- )
- @total_ordering
- @dataclass
- class SemanticVersion:
- """
- Semantische Versionsnummer.
- Implementiert SemVer 2.0.0 mit major.minor.patch[-prerelease][+build].
- Attributes:
- major: Hauptversion (inkompatible API-Änderungen)
- minor: Nebenversion (abwärtskompatible Funktionalität)
- patch: Patch-Version (abwärtskompatible Bugfixes)
- prerelease: Pre-Release-Bezeichner (alpha, beta, rc)
- build: Build-Metadaten
- Example:
- v1 = SemanticVersion(1, 2, 3)
- v2 = parse_version("1.2.4")
- if v2 > v1:
- print("v2 ist neuer")
- v3 = SemanticVersion(2, 0, 0, prerelease="alpha.1")
- print(v3) # 2.0.0-alpha.1
- """
- major: int
- minor: int
- patch: int
- prerelease: str | None = None
- build: str | None = None
- def __post_init__(self) -> None:
- """Validiert die Version."""
- if self.major < 0 or self.minor < 0 or self.patch < 0:
- raise ValueError("Version darf keine negativen Zahlen enthalten")
- def __str__(self) -> str:
- """Gibt die Version als String zurück."""
- version = f"{self.major}.{self.minor}.{self.patch}"
- if self.prerelease:
- version += f"-{self.prerelease}"
- if self.build:
- version += f"+{self.build}"
- return version
- def __repr__(self) -> str:
- return f"SemanticVersion({self})"
- def __hash__(self) -> int:
- return hash((self.major, self.minor, self.patch, self.prerelease))
- def __eq__(self, other: object) -> bool:
- if not isinstance(other, SemanticVersion):
- return NotImplemented
- return (
- self.major == other.major and
- self.minor == other.minor and
- self.patch == other.patch and
- self.prerelease == other.prerelease
- )
- def __lt__(self, other: SemanticVersion) -> bool:
- if not isinstance(other, SemanticVersion):
- return NotImplemented
- # Major, Minor, Patch vergleichen
- if (self.major, self.minor, self.patch) != (other.major, other.minor, other.patch):
- return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
- # Pre-release Vergleich
- # Eine Version ohne Pre-release ist größer als eine mit Pre-release
- if self.prerelease is None and other.prerelease is None:
- return False
- if self.prerelease is None:
- return False # Keine Pre-release > Pre-release
- if other.prerelease is None:
- return True # Pre-release < Keine Pre-release
- # Pre-release-Teile vergleichen
- return self._compare_prerelease(self.prerelease, other.prerelease) < 0
- @staticmethod
- def _compare_prerelease(a: str, b: str) -> int:
- """
- Vergleicht zwei Pre-release-Strings.
- Returns:
- -1 wenn a < b, 0 wenn gleich, 1 wenn a > b
- """
- parts_a = a.split(".")
- parts_b = b.split(".")
- for part_a, part_b in zip(parts_a, parts_b):
- # Numerische Teile numerisch vergleichen
- is_num_a = part_a.isdigit()
- is_num_b = part_b.isdigit()
- if is_num_a and is_num_b:
- num_a, num_b = int(part_a), int(part_b)
- if num_a != num_b:
- return -1 if num_a < num_b else 1
- elif is_num_a:
- return -1 # Numerisch < String
- elif is_num_b:
- return 1 # String > Numerisch
- else:
- if part_a != part_b:
- return -1 if part_a < part_b else 1
- # Längerer Pre-release hat höhere Priorität
- if len(parts_a) != len(parts_b):
- return -1 if len(parts_a) < len(parts_b) else 1
- return 0
- @property
- def is_prerelease(self) -> bool:
- """Ist dies eine Pre-Release-Version?"""
- return self.prerelease is not None
- @property
- def is_stable(self) -> bool:
- """Ist dies eine stabile Version (>= 1.0.0)?"""
- return self.major >= 1 and not self.is_prerelease
- def bump_major(self) -> SemanticVersion:
- """Gibt eine neue Version mit erhöhter Major-Version zurück."""
- return SemanticVersion(self.major + 1, 0, 0)
- def bump_minor(self) -> SemanticVersion:
- """Gibt eine neue Version mit erhöhter Minor-Version zurück."""
- return SemanticVersion(self.major, self.minor + 1, 0)
- def bump_patch(self) -> SemanticVersion:
- """Gibt eine neue Version mit erhöhter Patch-Version zurück."""
- return SemanticVersion(self.major, self.minor, self.patch + 1)
- def to_tuple(self) -> tuple[int, int, int]:
- """Gibt die Version als Tuple zurück."""
- return (self.major, self.minor, self.patch)
- def to_dict(self) -> dict[str, Any]:
- """Gibt die Version als Dictionary zurück."""
- return {
- "major": self.major,
- "minor": self.minor,
- "patch": self.patch,
- "prerelease": self.prerelease,
- "build": self.build,
- }
- class VersionRange:
- """
- Versionsbereich für Abhängigkeitsspezifikationen.
- Unterstützt verschiedene Operatoren:
- - Exakt: "1.2.3" oder "=1.2.3"
- - Größer: ">1.0.0", ">=1.0.0"
- - Kleiner: "<2.0.0", "<=2.0.0"
- - Bereich: ">=1.0.0 <2.0.0"
- - Caret: "^1.2.3" (kompatibel mit 1.x.x)
- - Tilde: "~1.2.3" (kompatibel mit 1.2.x)
- - Wildcard: "1.2.*", "1.*"
- Example:
- range1 = VersionRange("^1.2.0")
- assert range1.satisfies(parse_version("1.5.0"))
- assert not range1.satisfies(parse_version("2.0.0"))
- range2 = VersionRange(">=1.0.0 <2.0.0")
- assert range2.satisfies(parse_version("1.9.9"))
- """
- OPERATORS = [">=", "<=", "!=", ">", "<", "=", "^", "~"]
- def __init__(self, spec: str) -> None:
- """
- Initialisiert den Versionsbereich.
- Args:
- spec: Versionsspezifikation
- """
- self._spec = spec.strip()
- self._constraints = self._parse_spec(self._spec)
- def _parse_spec(self, spec: str) -> list[tuple[str, SemanticVersion]]:
- """Parst die Spezifikation in Constraints."""
- constraints = []
- # Wildcard-Handling
- if "*" in spec:
- return self._parse_wildcard(spec)
- # Mehrere Constraints (durch Leerzeichen oder Komma getrennt)
- parts = re.split(r"[,\s]+", spec)
- for part in parts:
- part = part.strip()
- if not part:
- continue
- # Operator extrahieren
- operator = "="
- version_str = part
- for op in sorted(self.OPERATORS, key=len, reverse=True):
- if part.startswith(op):
- operator = op
- version_str = part[len(op):].strip()
- break
- # Caret und Tilde zu Range konvertieren
- if operator == "^":
- constraints.extend(self._expand_caret(version_str))
- elif operator == "~":
- constraints.extend(self._expand_tilde(version_str))
- else:
- version = parse_version(version_str)
- constraints.append((operator, version))
- return constraints
- def _parse_wildcard(self, spec: str) -> list[tuple[str, SemanticVersion]]:
- """Parst Wildcard-Spezifikationen."""
- parts = spec.replace("*", "0").split(".")
- if len(parts) == 1:
- # "*" -> Alles
- return []
- major = int(parts[0]) if parts[0] != "0" or spec.startswith("0") else 0
- if len(parts) == 2 or (len(parts) >= 2 and parts[1] == "0" and "*" in spec):
- # "1.*" -> >=1.0.0 <2.0.0
- return [
- (">=", SemanticVersion(major, 0, 0)),
- ("<", SemanticVersion(major + 1, 0, 0)),
- ]
- minor = int(parts[1])
- # "1.2.*" -> >=1.2.0 <1.3.0
- return [
- (">=", SemanticVersion(major, minor, 0)),
- ("<", SemanticVersion(major, minor + 1, 0)),
- ]
- def _expand_caret(self, version_str: str) -> list[tuple[str, SemanticVersion]]:
- """Expandiert Caret-Range (^1.2.3 -> >=1.2.3 <2.0.0)."""
- version = parse_version(version_str)
- if version.major == 0:
- if version.minor == 0:
- # ^0.0.x -> =0.0.x
- upper = SemanticVersion(0, 0, version.patch + 1)
- else:
- # ^0.x.y -> >=0.x.y <0.(x+1).0
- upper = SemanticVersion(0, version.minor + 1, 0)
- else:
- # ^x.y.z -> >=x.y.z <(x+1).0.0
- upper = SemanticVersion(version.major + 1, 0, 0)
- return [(">=", version), ("<", upper)]
- def _expand_tilde(self, version_str: str) -> list[tuple[str, SemanticVersion]]:
- """Expandiert Tilde-Range (~1.2.3 -> >=1.2.3 <1.3.0)."""
- version = parse_version(version_str)
- upper = SemanticVersion(version.major, version.minor + 1, 0)
- return [(">=", version), ("<", upper)]
- def satisfies(self, version: SemanticVersion) -> bool:
- """
- Prüft, ob eine Version den Bereich erfüllt.
- Args:
- version: Zu prüfende Version
- Returns:
- True wenn die Version im Bereich liegt
- """
- if not self._constraints:
- return True # Leerer Bereich = Alles
- for operator, constraint in self._constraints:
- if not self._check_constraint(version, operator, constraint):
- return False
- return True
- def _check_constraint(
- self,
- version: SemanticVersion,
- operator: str,
- constraint: SemanticVersion
- ) -> bool:
- """Prüft einen einzelnen Constraint."""
- if operator == "=":
- return version == constraint
- elif operator == "!=":
- return version != constraint
- elif operator == ">":
- return version > constraint
- elif operator == ">=":
- return version >= constraint
- elif operator == "<":
- return version < constraint
- elif operator == "<=":
- return version <= constraint
- return False
- @property
- def spec(self) -> str:
- """Gibt die ursprüngliche Spezifikation zurück."""
- return self._spec
- def __str__(self) -> str:
- return self._spec
- def __repr__(self) -> str:
- return f"VersionRange({self._spec!r})"
- def __contains__(self, version: SemanticVersion) -> bool:
- return self.satisfies(version)
- def parse_version(version_str: str) -> SemanticVersion:
- """
- Parst einen Versions-String zu einer SemanticVersion.
- Unterstützt vollständige und vereinfachte Versionen:
- - "1.2.3"
- - "1.2.3-alpha.1"
- - "1.2.3+build.123"
- - "1.2" -> "1.2.0"
- - "1" -> "1.0.0"
- Args:
- version_str: Versions-String
- Returns:
- SemanticVersion-Objekt
- Raises:
- ValueError: Bei ungültigem Format
- """
- version_str = version_str.strip()
- if version_str.startswith("v") or version_str.startswith("V"):
- version_str = version_str[1:]
- # Erst vollständiges SemVer probieren
- match = SEMVER_PATTERN.match(version_str)
- if not match:
- # Einfacheres Pattern probieren
- match = SIMPLE_VERSION_PATTERN.match(version_str)
- if not match:
- raise ValueError(f"Ungültiges Versionsformat: {version_str}")
- groups = match.groupdict()
- major = int(groups["major"])
- minor = int(groups.get("minor") or 0)
- patch = int(groups.get("patch") or 0)
- prerelease = groups.get("prerelease")
- build = groups.get("build")
- return SemanticVersion(major, minor, patch, prerelease, build)
- def parse_range(spec: str) -> VersionRange:
- """
- Parst einen Range-String zu einem VersionRange.
- Args:
- spec: Range-Spezifikation
- Returns:
- VersionRange-Objekt
- """
- return VersionRange(spec)
|