compatibility.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. # -*- coding: utf-8 -*-
  2. """
  3. Versionskompatibilitätsprüfung.
  4. Prüft die Kompatibilität zwischen Plugin-Versionen und
  5. Abhängigkeitsanforderungen.
  6. """
  7. from __future__ import annotations
  8. from dataclasses import dataclass, field
  9. from enum import IntEnum, auto
  10. from typing import Any
  11. from trixy_core.plugins.versioning.semver import (
  12. SemanticVersion,
  13. VersionRange,
  14. parse_version,
  15. parse_range,
  16. )
  17. class CompatibilityLevel(IntEnum):
  18. """Kompatibilitätsgrad."""
  19. INCOMPATIBLE = auto() # Nicht kompatibel
  20. MAJOR_UPGRADE = auto() # Major-Version-Unterschied
  21. MINOR_UPGRADE = auto() # Minor-Version-Unterschied
  22. PATCH_UPGRADE = auto() # Nur Patch-Unterschied
  23. COMPATIBLE = auto() # Voll kompatibel
  24. EXACT = auto() # Exakt gleich
  25. @dataclass
  26. class CompatibilityResult:
  27. """
  28. Ergebnis einer Kompatibilitätsprüfung.
  29. Attributes:
  30. compatible: Ist kompatibel
  31. level: Kompatibilitätsgrad
  32. required: Angeforderte Version/Range
  33. available: Verfügbare Version
  34. message: Beschreibende Nachricht
  35. warnings: Optionale Warnungen
  36. """
  37. compatible: bool
  38. level: CompatibilityLevel
  39. required: str
  40. available: str
  41. message: str = ""
  42. warnings: list[str] = field(default_factory=list)
  43. def to_dict(self) -> dict[str, Any]:
  44. """Konvertiert zu Dictionary."""
  45. return {
  46. "compatible": self.compatible,
  47. "level": self.level.name,
  48. "required": self.required,
  49. "available": self.available,
  50. "message": self.message,
  51. "warnings": self.warnings,
  52. }
  53. class VersionChecker:
  54. """
  55. Prüft Versionskompatibilität.
  56. Vergleicht verfügbare Versionen mit Anforderungen und
  57. berechnet den Kompatibilitätsgrad.
  58. Example:
  59. checker = VersionChecker()
  60. # Einfache Prüfung
  61. result = checker.check(">=1.0.0 <2.0.0", "1.5.0")
  62. if result.compatible:
  63. print("Kompatibel!")
  64. # Plugin-Abhängigkeiten prüfen
  65. deps = {
  66. "core": "^2.0.0",
  67. "utils": "~1.5.0",
  68. }
  69. available = {
  70. "core": "2.1.0",
  71. "utils": "1.5.3",
  72. }
  73. results = checker.check_dependencies(deps, available)
  74. # API-Kompatibilität
  75. level = checker.get_compatibility_level("1.0.0", "2.0.0")
  76. if level == CompatibilityLevel.MAJOR_UPGRADE:
  77. print("Achtung: Möglicherweise inkompatible API-Änderungen")
  78. """
  79. def __init__(self, strict: bool = False) -> None:
  80. """
  81. Initialisiert den VersionChecker.
  82. Args:
  83. strict: Im strikten Modus sind Pre-Releases nicht kompatibel
  84. """
  85. self._strict = strict
  86. def check(
  87. self,
  88. required: str,
  89. available: str,
  90. ) -> CompatibilityResult:
  91. """
  92. Prüft, ob eine verfügbare Version die Anforderung erfüllt.
  93. Args:
  94. required: Versionsanforderung (z.B. "^1.0.0", ">=2.0.0 <3.0.0")
  95. available: Verfügbare Version
  96. Returns:
  97. CompatibilityResult
  98. """
  99. try:
  100. version_range = parse_range(required)
  101. version = parse_version(available)
  102. except ValueError as e:
  103. return CompatibilityResult(
  104. compatible=False,
  105. level=CompatibilityLevel.INCOMPATIBLE,
  106. required=required,
  107. available=available,
  108. message=f"Ungültiges Versionsformat: {e}",
  109. )
  110. # Pre-Release-Warnung im strikten Modus
  111. warnings = []
  112. if self._strict and version.is_prerelease:
  113. warnings.append("Pre-Release-Version im strikten Modus")
  114. # Prüfung
  115. is_compatible = version_range.satisfies(version)
  116. if is_compatible:
  117. # Kompatibilitätsgrad bestimmen
  118. level = self._determine_level(required, version)
  119. return CompatibilityResult(
  120. compatible=True,
  121. level=level,
  122. required=required,
  123. available=str(version),
  124. message="Versionsanforderung erfüllt",
  125. warnings=warnings,
  126. )
  127. else:
  128. return CompatibilityResult(
  129. compatible=False,
  130. level=CompatibilityLevel.INCOMPATIBLE,
  131. required=required,
  132. available=str(version),
  133. message=f"Version {version} erfüllt nicht {required}",
  134. warnings=warnings,
  135. )
  136. def _determine_level(
  137. self,
  138. required: str,
  139. available: SemanticVersion
  140. ) -> CompatibilityLevel:
  141. """Bestimmt den Kompatibilitätsgrad."""
  142. # Versuche, eine Basis-Version aus der Anforderung zu extrahieren
  143. try:
  144. # Für exakte Anforderungen
  145. if not any(op in required for op in ["^", "~", ">", "<", "*"]):
  146. base_version = parse_version(required.lstrip("="))
  147. if available == base_version:
  148. return CompatibilityLevel.EXACT
  149. elif available.major == base_version.major:
  150. if available.minor == base_version.minor:
  151. return CompatibilityLevel.PATCH_UPGRADE
  152. return CompatibilityLevel.MINOR_UPGRADE
  153. return CompatibilityLevel.MAJOR_UPGRADE
  154. except ValueError:
  155. pass
  156. return CompatibilityLevel.COMPATIBLE
  157. def get_compatibility_level(
  158. self,
  159. version1: str,
  160. version2: str,
  161. ) -> CompatibilityLevel:
  162. """
  163. Vergleicht zwei Versionen und gibt den Kompatibilitätsgrad zurück.
  164. Args:
  165. version1: Erste Version
  166. version2: Zweite Version
  167. Returns:
  168. Kompatibilitätsgrad
  169. """
  170. try:
  171. v1 = parse_version(version1)
  172. v2 = parse_version(version2)
  173. except ValueError:
  174. return CompatibilityLevel.INCOMPATIBLE
  175. if v1 == v2:
  176. return CompatibilityLevel.EXACT
  177. if v1.major != v2.major:
  178. return CompatibilityLevel.MAJOR_UPGRADE
  179. if v1.minor != v2.minor:
  180. return CompatibilityLevel.MINOR_UPGRADE
  181. if v1.patch != v2.patch:
  182. return CompatibilityLevel.PATCH_UPGRADE
  183. # Nur Pre-Release-Unterschied
  184. return CompatibilityLevel.COMPATIBLE
  185. def check_dependencies(
  186. self,
  187. required: dict[str, str],
  188. available: dict[str, str],
  189. ) -> dict[str, CompatibilityResult]:
  190. """
  191. Prüft mehrere Abhängigkeiten auf Kompatibilität.
  192. Args:
  193. required: Dictionary mit Name -> Versionsanforderung
  194. available: Dictionary mit Name -> Verfügbare Version
  195. Returns:
  196. Dictionary mit Name -> CompatibilityResult
  197. """
  198. results = {}
  199. for name, requirement in required.items():
  200. if name not in available:
  201. results[name] = CompatibilityResult(
  202. compatible=False,
  203. level=CompatibilityLevel.INCOMPATIBLE,
  204. required=requirement,
  205. available="(nicht verfügbar)",
  206. message=f"Abhängigkeit '{name}' nicht gefunden",
  207. )
  208. else:
  209. results[name] = self.check(requirement, available[name])
  210. return results
  211. def all_compatible(
  212. self,
  213. results: dict[str, CompatibilityResult]
  214. ) -> bool:
  215. """
  216. Prüft, ob alle Ergebnisse kompatibel sind.
  217. Args:
  218. results: Ergebnisse von check_dependencies
  219. Returns:
  220. True wenn alle kompatibel
  221. """
  222. return all(r.compatible for r in results.values())
  223. def find_incompatible(
  224. self,
  225. results: dict[str, CompatibilityResult]
  226. ) -> list[str]:
  227. """
  228. Findet alle inkompatiblen Abhängigkeiten.
  229. Args:
  230. results: Ergebnisse von check_dependencies
  231. Returns:
  232. Liste der inkompatiblen Namen
  233. """
  234. return [name for name, result in results.items() if not result.compatible]
  235. def is_upgrade(
  236. self,
  237. current: str,
  238. target: str,
  239. ) -> bool:
  240. """
  241. Prüft, ob target ein Upgrade von current ist.
  242. Args:
  243. current: Aktuelle Version
  244. target: Zielversion
  245. Returns:
  246. True wenn target > current
  247. """
  248. try:
  249. v_current = parse_version(current)
  250. v_target = parse_version(target)
  251. return v_target > v_current
  252. except ValueError:
  253. return False
  254. def is_breaking_change(
  255. self,
  256. current: str,
  257. target: str,
  258. ) -> bool:
  259. """
  260. Prüft, ob target ein Breaking Change ist (Major-Version-Änderung).
  261. Args:
  262. current: Aktuelle Version
  263. target: Zielversion
  264. Returns:
  265. True bei Breaking Change
  266. """
  267. level = self.get_compatibility_level(current, target)
  268. return level == CompatibilityLevel.MAJOR_UPGRADE
  269. def suggest_compatible_version(
  270. self,
  271. required: str,
  272. available_versions: list[str],
  273. ) -> str | None:
  274. """
  275. Schlägt die beste kompatible Version vor.
  276. Args:
  277. required: Versionsanforderung
  278. available_versions: Liste verfügbarer Versionen
  279. Returns:
  280. Beste kompatible Version oder None
  281. """
  282. try:
  283. version_range = parse_range(required)
  284. except ValueError:
  285. return None
  286. compatible = []
  287. for version_str in available_versions:
  288. try:
  289. version = parse_version(version_str)
  290. if version_range.satisfies(version):
  291. compatible.append((version, version_str))
  292. except ValueError:
  293. continue
  294. if not compatible:
  295. return None
  296. # Höchste kompatible Version zurückgeben
  297. compatible.sort(key=lambda x: x[0], reverse=True)
  298. return compatible[0][1]