resolver.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. # -*- coding: utf-8 -*-
  2. """
  3. Abhängigkeitsauflösung für Plugins.
  4. Löst Abhängigkeiten auf und berechnet die optimale
  5. Plugin-Ladereihenfolge.
  6. """
  7. from __future__ import annotations
  8. from dataclasses import dataclass, field
  9. from typing import Any
  10. from trixy_core.plugins.dependencies.graph import (
  11. DependencyGraph,
  12. DependencyNode,
  13. CycleError,
  14. )
  15. from trixy_core.plugins.versioning.semver import parse_version
  16. from trixy_core.plugins.versioning.compatibility import (
  17. VersionChecker,
  18. CompatibilityResult,
  19. )
  20. class ResolutionError(Exception):
  21. """Fehler bei der Abhängigkeitsauflösung."""
  22. def __init__(
  23. self,
  24. message: str,
  25. missing: list[str] | None = None,
  26. incompatible: dict[str, str] | None = None,
  27. cycle: list[str] | None = None,
  28. ) -> None:
  29. super().__init__(message)
  30. self.missing = missing or []
  31. self.incompatible = incompatible or {}
  32. self.cycle = cycle
  33. @dataclass
  34. class Dependency:
  35. """
  36. Eine Plugin-Abhängigkeit.
  37. Attributes:
  38. name: Name des Plugins
  39. version_requirement: Versionsanforderung
  40. optional: Ist optional
  41. reason: Grund für die Abhängigkeit
  42. """
  43. name: str
  44. version_requirement: str = "*"
  45. optional: bool = False
  46. reason: str = ""
  47. def to_dict(self) -> dict[str, Any]:
  48. """Konvertiert zu Dictionary."""
  49. return {
  50. "name": self.name,
  51. "version_requirement": self.version_requirement,
  52. "optional": self.optional,
  53. "reason": self.reason,
  54. }
  55. @dataclass
  56. class ResolutionResult:
  57. """
  58. Ergebnis einer Abhängigkeitsauflösung.
  59. Attributes:
  60. success: Auflösung erfolgreich
  61. load_order: Ladereihenfolge
  62. resolved: Aufgelöste Abhängigkeiten
  63. missing: Fehlende Abhängigkeiten
  64. incompatible: Inkompatible Versionen
  65. warnings: Warnungen
  66. """
  67. success: bool
  68. load_order: list[str] = field(default_factory=list)
  69. resolved: dict[str, str] = field(default_factory=dict) # name -> version
  70. missing: list[str] = field(default_factory=list)
  71. incompatible: dict[str, CompatibilityResult] = field(default_factory=dict)
  72. warnings: list[str] = field(default_factory=list)
  73. def to_dict(self) -> dict[str, Any]:
  74. """Konvertiert zu Dictionary."""
  75. return {
  76. "success": self.success,
  77. "load_order": self.load_order,
  78. "resolved": self.resolved,
  79. "missing": self.missing,
  80. "incompatible": {
  81. k: v.to_dict() for k, v in self.incompatible.items()
  82. },
  83. "warnings": self.warnings,
  84. }
  85. class DependencyResolver:
  86. """
  87. Löst Plugin-Abhängigkeiten auf.
  88. Erstellt einen Abhängigkeitsgraphen, prüft Versionskompatibilität
  89. und berechnet die optimale Ladereihenfolge.
  90. Example:
  91. resolver = DependencyResolver()
  92. # Plugin-Informationen registrieren
  93. resolver.register_plugin(
  94. "my_plugin",
  95. version="1.0.0",
  96. dependencies={
  97. "core_plugin": "^2.0.0",
  98. "util_plugin": "~1.5.0",
  99. }
  100. )
  101. resolver.register_plugin("core_plugin", "2.1.0")
  102. resolver.register_plugin("util_plugin", "1.5.3")
  103. # Auflösen
  104. result = resolver.resolve()
  105. if result.success:
  106. for plugin in result.load_order:
  107. load_plugin(plugin)
  108. else:
  109. print(f"Fehler: {result.missing}, {result.incompatible}")
  110. # Visualisierung
  111. print(resolver.graph.to_dot())
  112. """
  113. def __init__(self, strict: bool = False) -> None:
  114. """
  115. Initialisiert den Resolver.
  116. Args:
  117. strict: Strikte Versionsprüfung
  118. """
  119. self._graph = DependencyGraph()
  120. self._checker = VersionChecker(strict=strict)
  121. self._available: dict[str, str] = {} # name -> version
  122. @property
  123. def graph(self) -> DependencyGraph:
  124. """Der Abhängigkeitsgraph."""
  125. return self._graph
  126. def register_plugin(
  127. self,
  128. name: str,
  129. version: str = "1.0.0",
  130. dependencies: dict[str, str] | None = None,
  131. optional_dependencies: dict[str, str] | None = None,
  132. metadata: dict[str, Any] | None = None,
  133. ) -> DependencyNode:
  134. """
  135. Registriert ein Plugin.
  136. Args:
  137. name: Plugin-Name
  138. version: Plugin-Version
  139. dependencies: Erforderliche Abhängigkeiten
  140. optional_dependencies: Optionale Abhängigkeiten
  141. metadata: Zusätzliche Metadaten
  142. Returns:
  143. Der erstellte Knoten
  144. """
  145. node = DependencyNode(
  146. name=name,
  147. version=version,
  148. dependencies=dependencies or {},
  149. optional_dependencies=optional_dependencies or {},
  150. metadata=metadata or {},
  151. )
  152. if self._graph.has_node(name):
  153. self._graph.update_node(node)
  154. else:
  155. self._graph.add_node(node)
  156. self._available[name] = version
  157. return node
  158. def unregister_plugin(self, name: str) -> bool:
  159. """
  160. Entfernt ein Plugin.
  161. Args:
  162. name: Plugin-Name
  163. Returns:
  164. True wenn entfernt
  165. """
  166. if self._graph.remove_node(name):
  167. self._available.pop(name, None)
  168. return True
  169. return False
  170. def resolve(
  171. self,
  172. targets: list[str] | None = None,
  173. include_optional: bool = False,
  174. ) -> ResolutionResult:
  175. """
  176. Löst Abhängigkeiten auf.
  177. Args:
  178. targets: Spezifische Plugins zum Auflösen (None = alle)
  179. include_optional: Optionale Abhängigkeiten einbeziehen
  180. Returns:
  181. ResolutionResult
  182. """
  183. warnings: list[str] = []
  184. missing: list[str] = []
  185. incompatible: dict[str, CompatibilityResult] = {}
  186. # Fehlende Abhängigkeiten finden
  187. missing_deps = self._graph.get_missing_dependencies()
  188. for plugin, deps in missing_deps.items():
  189. for dep in deps:
  190. node = self._graph.get_node(plugin)
  191. if node and dep in node.optional_dependencies:
  192. if include_optional:
  193. warnings.append(
  194. f"Optionale Abhängigkeit '{dep}' für '{plugin}' nicht gefunden"
  195. )
  196. else:
  197. missing.append(f"{dep} (benötigt von {plugin})")
  198. # Versionskompatibilität prüfen
  199. for node in self._graph:
  200. for dep_name, requirement in node.dependencies.items():
  201. if dep_name not in self._available:
  202. continue
  203. result = self._checker.check(requirement, self._available[dep_name])
  204. if not result.compatible:
  205. incompatible[f"{node.name}->{dep_name}"] = result
  206. elif result.warnings:
  207. for warning in result.warnings:
  208. warnings.append(f"{node.name}->{dep_name}: {warning}")
  209. # Zyklus prüfen
  210. try:
  211. load_order = self._graph.get_load_order()
  212. # Nur angeforderte Plugins filtern
  213. if targets:
  214. needed = set(targets)
  215. for target in targets:
  216. needed.update(self._graph.get_all_dependencies(target))
  217. load_order = [p for p in load_order if p in needed]
  218. except CycleError as e:
  219. return ResolutionResult(
  220. success=False,
  221. missing=missing,
  222. incompatible=incompatible,
  223. warnings=[f"Zyklische Abhängigkeit: {' -> '.join(e.cycle)}"],
  224. )
  225. # Erfolg bestimmen
  226. success = not missing and not incompatible
  227. return ResolutionResult(
  228. success=success,
  229. load_order=load_order,
  230. resolved={name: self._available[name] for name in load_order if name in self._available},
  231. missing=missing,
  232. incompatible=incompatible,
  233. warnings=warnings,
  234. )
  235. def check_can_install(
  236. self,
  237. name: str,
  238. version: str,
  239. dependencies: dict[str, str] | None = None,
  240. ) -> ResolutionResult:
  241. """
  242. Prüft, ob ein Plugin installiert werden kann.
  243. Args:
  244. name: Plugin-Name
  245. version: Plugin-Version
  246. dependencies: Plugin-Abhängigkeiten
  247. Returns:
  248. ResolutionResult
  249. """
  250. dependencies = dependencies or {}
  251. missing = []
  252. incompatible: dict[str, CompatibilityResult] = {}
  253. for dep_name, requirement in dependencies.items():
  254. if dep_name not in self._available:
  255. missing.append(dep_name)
  256. else:
  257. result = self._checker.check(requirement, self._available[dep_name])
  258. if not result.compatible:
  259. incompatible[dep_name] = result
  260. success = not missing and not incompatible
  261. return ResolutionResult(
  262. success=success,
  263. missing=missing,
  264. incompatible=incompatible,
  265. )
  266. def check_can_uninstall(self, name: str) -> tuple[bool, list[str]]:
  267. """
  268. Prüft, ob ein Plugin deinstalliert werden kann.
  269. Args:
  270. name: Plugin-Name
  271. Returns:
  272. (Kann deinstalliert werden, Liste abhängiger Plugins)
  273. """
  274. dependents = list(self._graph.get_dependents(name))
  275. return (not dependents, dependents)
  276. def get_install_order(self, plugins: list[str]) -> list[str]:
  277. """
  278. Berechnet die Installationsreihenfolge für mehrere Plugins.
  279. Args:
  280. plugins: Liste der zu installierenden Plugins
  281. Returns:
  282. Sortierte Installationsreihenfolge
  283. """
  284. needed: set[str] = set(plugins)
  285. for plugin in plugins:
  286. needed.update(self._graph.get_all_dependencies(plugin))
  287. order = self._graph.get_load_order()
  288. return [p for p in order if p in needed]
  289. def get_uninstall_order(self, plugins: list[str]) -> list[str]:
  290. """
  291. Berechnet die Deinstallationsreihenfolge.
  292. Args:
  293. plugins: Liste der zu entfernenden Plugins
  294. Returns:
  295. Sortierte Deinstallationsreihenfolge
  296. """
  297. to_remove: set[str] = set(plugins)
  298. # Alle abhängigen auch entfernen
  299. for plugin in plugins:
  300. to_remove.update(self._graph.get_all_dependents(plugin))
  301. order = self._graph.get_unload_order()
  302. return [p for p in order if p in to_remove]
  303. def get_affected_by_update(
  304. self,
  305. name: str,
  306. new_version: str,
  307. ) -> list[str]:
  308. """
  309. Findet alle Plugins, die von einem Update betroffen sind.
  310. Args:
  311. name: Plugin-Name
  312. new_version: Neue Version
  313. Returns:
  314. Liste betroffener Plugins
  315. """
  316. affected = []
  317. dependents = self._graph.get_all_dependents(name)
  318. for dependent in dependents:
  319. node = self._graph.get_node(dependent)
  320. if node and name in node.dependencies:
  321. requirement = node.dependencies[name]
  322. result = self._checker.check(requirement, new_version)
  323. if not result.compatible:
  324. affected.append(dependent)
  325. return affected
  326. def clear(self) -> None:
  327. """Löscht alle registrierten Plugins."""
  328. self._graph = DependencyGraph()
  329. self._available.clear()
  330. def get_statistics(self) -> dict[str, Any]:
  331. """Gibt Statistiken zurück."""
  332. return {
  333. "total_plugins": self._graph.node_count,
  334. "total_dependencies": self._graph.edge_count,
  335. "root_plugins": len(self._graph.get_root_nodes()),
  336. "leaf_plugins": len(self._graph.get_leaf_nodes()),
  337. "has_cycles": self._graph.has_cycle(),
  338. "missing_dependencies": sum(
  339. len(deps) for deps in self._graph.get_missing_dependencies().values()
  340. ),
  341. }