# -*- 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]