| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- # -*- coding: utf-8 -*-
- """
- Versionskompatibilitätsprüfung.
- Prüft die Kompatibilität zwischen Plugin-Versionen und
- Abhängigkeitsanforderungen.
- """
- from __future__ import annotations
- from dataclasses import dataclass, field
- from enum import IntEnum, auto
- from typing import Any
- from trixy_core.plugins.versioning.semver import (
- SemanticVersion,
- VersionRange,
- parse_version,
- parse_range,
- )
- class CompatibilityLevel(IntEnum):
- """Kompatibilitätsgrad."""
- INCOMPATIBLE = auto() # Nicht kompatibel
- MAJOR_UPGRADE = auto() # Major-Version-Unterschied
- MINOR_UPGRADE = auto() # Minor-Version-Unterschied
- PATCH_UPGRADE = auto() # Nur Patch-Unterschied
- COMPATIBLE = auto() # Voll kompatibel
- EXACT = auto() # Exakt gleich
- @dataclass
- class CompatibilityResult:
- """
- Ergebnis einer Kompatibilitätsprüfung.
- Attributes:
- compatible: Ist kompatibel
- level: Kompatibilitätsgrad
- required: Angeforderte Version/Range
- available: Verfügbare Version
- message: Beschreibende Nachricht
- warnings: Optionale Warnungen
- """
- compatible: bool
- level: CompatibilityLevel
- required: str
- available: str
- message: str = ""
- warnings: list[str] = field(default_factory=list)
- def to_dict(self) -> dict[str, Any]:
- """Konvertiert zu Dictionary."""
- return {
- "compatible": self.compatible,
- "level": self.level.name,
- "required": self.required,
- "available": self.available,
- "message": self.message,
- "warnings": self.warnings,
- }
- class VersionChecker:
- """
- Prüft Versionskompatibilität.
- Vergleicht verfügbare Versionen mit Anforderungen und
- berechnet den Kompatibilitätsgrad.
- Example:
- checker = VersionChecker()
- # Einfache Prüfung
- result = checker.check(">=1.0.0 <2.0.0", "1.5.0")
- if result.compatible:
- print("Kompatibel!")
- # Plugin-Abhängigkeiten prüfen
- deps = {
- "core": "^2.0.0",
- "utils": "~1.5.0",
- }
- available = {
- "core": "2.1.0",
- "utils": "1.5.3",
- }
- results = checker.check_dependencies(deps, available)
- # API-Kompatibilität
- level = checker.get_compatibility_level("1.0.0", "2.0.0")
- if level == CompatibilityLevel.MAJOR_UPGRADE:
- print("Achtung: Möglicherweise inkompatible API-Änderungen")
- """
- def __init__(self, strict: bool = False) -> None:
- """
- Initialisiert den VersionChecker.
- Args:
- strict: Im strikten Modus sind Pre-Releases nicht kompatibel
- """
- self._strict = strict
- def check(
- self,
- required: str,
- available: str,
- ) -> CompatibilityResult:
- """
- Prüft, ob eine verfügbare Version die Anforderung erfüllt.
- Args:
- required: Versionsanforderung (z.B. "^1.0.0", ">=2.0.0 <3.0.0")
- available: Verfügbare Version
- Returns:
- CompatibilityResult
- """
- try:
- version_range = parse_range(required)
- version = parse_version(available)
- except ValueError as e:
- return CompatibilityResult(
- compatible=False,
- level=CompatibilityLevel.INCOMPATIBLE,
- required=required,
- available=available,
- message=f"Ungültiges Versionsformat: {e}",
- )
- # Pre-Release-Warnung im strikten Modus
- warnings = []
- if self._strict and version.is_prerelease:
- warnings.append("Pre-Release-Version im strikten Modus")
- # Prüfung
- is_compatible = version_range.satisfies(version)
- if is_compatible:
- # Kompatibilitätsgrad bestimmen
- level = self._determine_level(required, version)
- return CompatibilityResult(
- compatible=True,
- level=level,
- required=required,
- available=str(version),
- message="Versionsanforderung erfüllt",
- warnings=warnings,
- )
- else:
- return CompatibilityResult(
- compatible=False,
- level=CompatibilityLevel.INCOMPATIBLE,
- required=required,
- available=str(version),
- message=f"Version {version} erfüllt nicht {required}",
- warnings=warnings,
- )
- def _determine_level(
- self,
- required: str,
- available: SemanticVersion
- ) -> CompatibilityLevel:
- """Bestimmt den Kompatibilitätsgrad."""
- # Versuche, eine Basis-Version aus der Anforderung zu extrahieren
- try:
- # Für exakte Anforderungen
- if not any(op in required for op in ["^", "~", ">", "<", "*"]):
- base_version = parse_version(required.lstrip("="))
- if available == base_version:
- return CompatibilityLevel.EXACT
- elif available.major == base_version.major:
- if available.minor == base_version.minor:
- return CompatibilityLevel.PATCH_UPGRADE
- return CompatibilityLevel.MINOR_UPGRADE
- return CompatibilityLevel.MAJOR_UPGRADE
- except ValueError:
- pass
- return CompatibilityLevel.COMPATIBLE
- def get_compatibility_level(
- self,
- version1: str,
- version2: str,
- ) -> CompatibilityLevel:
- """
- Vergleicht zwei Versionen und gibt den Kompatibilitätsgrad zurück.
- Args:
- version1: Erste Version
- version2: Zweite Version
- Returns:
- Kompatibilitätsgrad
- """
- try:
- v1 = parse_version(version1)
- v2 = parse_version(version2)
- except ValueError:
- return CompatibilityLevel.INCOMPATIBLE
- if v1 == v2:
- return CompatibilityLevel.EXACT
- if v1.major != v2.major:
- return CompatibilityLevel.MAJOR_UPGRADE
- if v1.minor != v2.minor:
- return CompatibilityLevel.MINOR_UPGRADE
- if v1.patch != v2.patch:
- return CompatibilityLevel.PATCH_UPGRADE
- # Nur Pre-Release-Unterschied
- return CompatibilityLevel.COMPATIBLE
- def check_dependencies(
- self,
- required: dict[str, str],
- available: dict[str, str],
- ) -> dict[str, CompatibilityResult]:
- """
- Prüft mehrere Abhängigkeiten auf Kompatibilität.
- Args:
- required: Dictionary mit Name -> Versionsanforderung
- available: Dictionary mit Name -> Verfügbare Version
- Returns:
- Dictionary mit Name -> CompatibilityResult
- """
- results = {}
- for name, requirement in required.items():
- if name not in available:
- results[name] = CompatibilityResult(
- compatible=False,
- level=CompatibilityLevel.INCOMPATIBLE,
- required=requirement,
- available="(nicht verfügbar)",
- message=f"Abhängigkeit '{name}' nicht gefunden",
- )
- else:
- results[name] = self.check(requirement, available[name])
- return results
- def all_compatible(
- self,
- results: dict[str, CompatibilityResult]
- ) -> bool:
- """
- Prüft, ob alle Ergebnisse kompatibel sind.
- Args:
- results: Ergebnisse von check_dependencies
- Returns:
- True wenn alle kompatibel
- """
- return all(r.compatible for r in results.values())
- def find_incompatible(
- self,
- results: dict[str, CompatibilityResult]
- ) -> list[str]:
- """
- Findet alle inkompatiblen Abhängigkeiten.
- Args:
- results: Ergebnisse von check_dependencies
- Returns:
- Liste der inkompatiblen Namen
- """
- return [name for name, result in results.items() if not result.compatible]
- def is_upgrade(
- self,
- current: str,
- target: str,
- ) -> bool:
- """
- Prüft, ob target ein Upgrade von current ist.
- Args:
- current: Aktuelle Version
- target: Zielversion
- Returns:
- True wenn target > current
- """
- try:
- v_current = parse_version(current)
- v_target = parse_version(target)
- return v_target > v_current
- except ValueError:
- return False
- def is_breaking_change(
- self,
- current: str,
- target: str,
- ) -> bool:
- """
- Prüft, ob target ein Breaking Change ist (Major-Version-Änderung).
- Args:
- current: Aktuelle Version
- target: Zielversion
- Returns:
- True bei Breaking Change
- """
- level = self.get_compatibility_level(current, target)
- return level == CompatibilityLevel.MAJOR_UPGRADE
- def suggest_compatible_version(
- self,
- required: str,
- available_versions: list[str],
- ) -> str | None:
- """
- Schlägt die beste kompatible Version vor.
- Args:
- required: Versionsanforderung
- available_versions: Liste verfügbarer Versionen
- Returns:
- Beste kompatible Version oder None
- """
- try:
- version_range = parse_range(required)
- except ValueError:
- return None
- compatible = []
- for version_str in available_versions:
- try:
- version = parse_version(version_str)
- if version_range.satisfies(version):
- compatible.append((version, version_str))
- except ValueError:
- continue
- if not compatible:
- return None
- # Höchste kompatible Version zurückgeben
- compatible.sort(key=lambda x: x[0], reverse=True)
- return compatible[0][1]
|