hook.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. # -*- coding: utf-8 -*-
  2. """
  3. Plugin-Hook Implementierung.
  4. Hooks ermöglichen Plugins, sich in definierte Erweiterungspunkte
  5. einzuklinken und Daten zu modifizieren.
  6. """
  7. from __future__ import annotations
  8. import asyncio
  9. from dataclasses import dataclass, field
  10. from datetime import datetime
  11. from enum import IntEnum, auto
  12. from typing import Any, Callable, TypeVar, Generic
  13. T = TypeVar("T")
  14. # Hook-Callback-Typen
  15. HookCallback = Callable[..., Any]
  16. AsyncHookCallback = Callable[..., Any] # Koroutine
  17. class HookPriority(IntEnum):
  18. """Priorität für Hook-Callbacks."""
  19. FIRST = 0 # Wird zuerst ausgeführt
  20. HIGH = 25
  21. NORMAL = 50 # Standard
  22. LOW = 75
  23. LAST = 100 # Wird zuletzt ausgeführt
  24. @dataclass
  25. class HookResult(Generic[T]):
  26. """
  27. Ergebnis eines Hook-Aufrufs.
  28. Attributes:
  29. value: Der Ergebniswert
  30. modified: Wurde der Wert modifiziert
  31. stopped: Wurde die Verarbeitung gestoppt
  32. source: Welches Plugin hat modifiziert
  33. errors: Aufgetretene Fehler
  34. """
  35. value: T
  36. modified: bool = False
  37. stopped: bool = False
  38. source: str = ""
  39. errors: list[str] = field(default_factory=list)
  40. @property
  41. def success(self) -> bool:
  42. """Keine Fehler aufgetreten."""
  43. return len(self.errors) == 0
  44. @dataclass
  45. class HookHandler:
  46. """
  47. Ein registrierter Hook-Handler.
  48. Attributes:
  49. callback: Die Callback-Funktion
  50. plugin_name: Name des registrierenden Plugins
  51. priority: Ausführungspriorität
  52. is_async: Ist asynchron
  53. once: Nur einmal ausführen
  54. enabled: Ist aktiviert
  55. call_count: Anzahl Aufrufe
  56. """
  57. callback: HookCallback
  58. plugin_name: str
  59. priority: HookPriority = HookPriority.NORMAL
  60. is_async: bool = False
  61. once: bool = False
  62. enabled: bool = True
  63. call_count: int = 0
  64. created_at: datetime = field(default_factory=datetime.now)
  65. def __lt__(self, other: HookHandler) -> bool:
  66. """Sortierung nach Priorität."""
  67. return self.priority < other.priority
  68. class PluginHook(Generic[T]):
  69. """
  70. Ein Plugin-Hook für Erweiterungspunkte.
  71. Hooks ermöglichen Plugins, sich in definierte Punkte einzuklinken
  72. und Daten zu verarbeiten oder zu modifizieren.
  73. Es gibt zwei Hauptmuster:
  74. 1. Filter: Modifiziert einen Wert (z.B. Text-Transformation)
  75. 2. Action: Führt eine Aktion aus (z.B. Logging)
  76. Example:
  77. # Hook definieren
  78. process_text = PluginHook[str]("process_text")
  79. # Handler registrieren
  80. @process_text.register("my_plugin")
  81. def uppercase(text: str) -> str:
  82. return text.upper()
  83. @process_text.register("other_plugin", priority=HookPriority.LAST)
  84. def add_period(text: str) -> str:
  85. if not text.endswith("."):
  86. return text + "."
  87. return text
  88. # Hook ausführen
  89. result = process_text.apply("hello world")
  90. print(result.value) # "HELLO WORLD."
  91. # Async-Unterstützung
  92. @process_text.register_async("async_plugin")
  93. async def async_handler(text: str) -> str:
  94. await asyncio.sleep(0.1)
  95. return text.lower()
  96. result = await process_text.apply_async("HELLO")
  97. """
  98. def __init__(
  99. self,
  100. name: str,
  101. description: str = "",
  102. default_value: T | None = None,
  103. ) -> None:
  104. """
  105. Initialisiert den Hook.
  106. Args:
  107. name: Eindeutiger Hook-Name
  108. description: Beschreibung
  109. default_value: Standardwert wenn keine Handler
  110. """
  111. self._name = name
  112. self._description = description
  113. self._default_value = default_value
  114. self._handlers: list[HookHandler] = []
  115. @property
  116. def name(self) -> str:
  117. """Hook-Name."""
  118. return self._name
  119. @property
  120. def description(self) -> str:
  121. """Hook-Beschreibung."""
  122. return self._description
  123. @property
  124. def handler_count(self) -> int:
  125. """Anzahl registrierter Handler."""
  126. return len(self._handlers)
  127. def register(
  128. self,
  129. plugin_name: str,
  130. priority: HookPriority = HookPriority.NORMAL,
  131. once: bool = False,
  132. ) -> Callable[[HookCallback], HookCallback]:
  133. """
  134. Decorator zum Registrieren eines Handlers.
  135. Args:
  136. plugin_name: Name des Plugins
  137. priority: Ausführungspriorität
  138. once: Nur einmal ausführen
  139. Returns:
  140. Decorator-Funktion
  141. """
  142. def decorator(callback: HookCallback) -> HookCallback:
  143. handler = HookHandler(
  144. callback=callback,
  145. plugin_name=plugin_name,
  146. priority=priority,
  147. is_async=asyncio.iscoroutinefunction(callback),
  148. once=once,
  149. )
  150. self._handlers.append(handler)
  151. self._handlers.sort()
  152. return callback
  153. return decorator
  154. def register_async(
  155. self,
  156. plugin_name: str,
  157. priority: HookPriority = HookPriority.NORMAL,
  158. once: bool = False,
  159. ) -> Callable[[AsyncHookCallback], AsyncHookCallback]:
  160. """Decorator für async Handler."""
  161. return self.register(plugin_name, priority, once)
  162. def add_handler(
  163. self,
  164. callback: HookCallback,
  165. plugin_name: str,
  166. priority: HookPriority = HookPriority.NORMAL,
  167. once: bool = False,
  168. ) -> None:
  169. """Fügt einen Handler direkt hinzu."""
  170. handler = HookHandler(
  171. callback=callback,
  172. plugin_name=plugin_name,
  173. priority=priority,
  174. is_async=asyncio.iscoroutinefunction(callback),
  175. once=once,
  176. )
  177. self._handlers.append(handler)
  178. self._handlers.sort()
  179. def remove_handler(self, plugin_name: str) -> int:
  180. """
  181. Entfernt alle Handler eines Plugins.
  182. Args:
  183. plugin_name: Plugin-Name
  184. Returns:
  185. Anzahl entfernter Handler
  186. """
  187. before = len(self._handlers)
  188. self._handlers = [h for h in self._handlers if h.plugin_name != plugin_name]
  189. return before - len(self._handlers)
  190. def apply(self, value: T, *args: Any, **kwargs: Any) -> HookResult[T]:
  191. """
  192. Wendet den Hook auf einen Wert an.
  193. Args:
  194. value: Der zu verarbeitende Wert
  195. *args: Zusätzliche Argumente
  196. **kwargs: Zusätzliche Keyword-Argumente
  197. Returns:
  198. HookResult mit dem verarbeiteten Wert
  199. """
  200. current_value = value
  201. modified = False
  202. errors: list[str] = []
  203. source = ""
  204. to_remove: list[HookHandler] = []
  205. for handler in self._handlers:
  206. if not handler.enabled:
  207. continue
  208. try:
  209. if handler.is_async:
  210. # Async-Handler synchron ausführen (wenn möglich)
  211. try:
  212. loop = asyncio.get_event_loop()
  213. result = loop.run_until_complete(
  214. handler.callback(current_value, *args, **kwargs)
  215. )
  216. except RuntimeError:
  217. # Kein Event-Loop - überspringen
  218. errors.append(
  219. f"{handler.plugin_name}: Async-Handler ohne Event-Loop"
  220. )
  221. continue
  222. else:
  223. result = handler.callback(current_value, *args, **kwargs)
  224. handler.call_count += 1
  225. if result is not None:
  226. current_value = result
  227. modified = True
  228. source = handler.plugin_name
  229. if handler.once:
  230. to_remove.append(handler)
  231. except Exception as e:
  232. errors.append(f"{handler.plugin_name}: {str(e)}")
  233. # Once-Handler entfernen
  234. for handler in to_remove:
  235. self._handlers.remove(handler)
  236. return HookResult(
  237. value=current_value,
  238. modified=modified,
  239. source=source,
  240. errors=errors,
  241. )
  242. async def apply_async(
  243. self,
  244. value: T,
  245. *args: Any,
  246. **kwargs: Any,
  247. ) -> HookResult[T]:
  248. """
  249. Wendet den Hook asynchron an.
  250. Args:
  251. value: Der zu verarbeitende Wert
  252. *args: Zusätzliche Argumente
  253. **kwargs: Zusätzliche Keyword-Argumente
  254. Returns:
  255. HookResult mit dem verarbeiteten Wert
  256. """
  257. current_value = value
  258. modified = False
  259. errors: list[str] = []
  260. source = ""
  261. to_remove: list[HookHandler] = []
  262. for handler in self._handlers:
  263. if not handler.enabled:
  264. continue
  265. try:
  266. if handler.is_async:
  267. result = await handler.callback(current_value, *args, **kwargs)
  268. else:
  269. result = handler.callback(current_value, *args, **kwargs)
  270. handler.call_count += 1
  271. if result is not None:
  272. current_value = result
  273. modified = True
  274. source = handler.plugin_name
  275. if handler.once:
  276. to_remove.append(handler)
  277. except Exception as e:
  278. errors.append(f"{handler.plugin_name}: {str(e)}")
  279. # Once-Handler entfernen
  280. for handler in to_remove:
  281. self._handlers.remove(handler)
  282. return HookResult(
  283. value=current_value,
  284. modified=modified,
  285. source=source,
  286. errors=errors,
  287. )
  288. def trigger(self, *args: Any, **kwargs: Any) -> list[Any]:
  289. """
  290. Triggert den Hook als Action (ohne Rückgabewert-Verkettung).
  291. Args:
  292. *args: Argumente für Handler
  293. **kwargs: Keyword-Argumente
  294. Returns:
  295. Liste der Rückgabewerte
  296. """
  297. results = []
  298. for handler in self._handlers:
  299. if not handler.enabled:
  300. continue
  301. try:
  302. if handler.is_async:
  303. continue # Async-Handler überspringen
  304. result = handler.callback(*args, **kwargs)
  305. handler.call_count += 1
  306. results.append(result)
  307. if handler.once:
  308. self._handlers.remove(handler)
  309. except Exception:
  310. pass
  311. return results
  312. async def trigger_async(self, *args: Any, **kwargs: Any) -> list[Any]:
  313. """Triggert den Hook asynchron."""
  314. results = []
  315. for handler in list(self._handlers):
  316. if not handler.enabled:
  317. continue
  318. try:
  319. if handler.is_async:
  320. result = await handler.callback(*args, **kwargs)
  321. else:
  322. result = handler.callback(*args, **kwargs)
  323. handler.call_count += 1
  324. results.append(result)
  325. if handler.once:
  326. self._handlers.remove(handler)
  327. except Exception:
  328. pass
  329. return results
  330. def clear(self) -> int:
  331. """
  332. Entfernt alle Handler.
  333. Returns:
  334. Anzahl entfernter Handler
  335. """
  336. count = len(self._handlers)
  337. self._handlers.clear()
  338. return count
  339. def get_handlers(self) -> list[HookHandler]:
  340. """Gibt alle Handler zurück."""
  341. return self._handlers.copy()
  342. def __repr__(self) -> str:
  343. return f"PluginHook({self._name!r}, handlers={len(self._handlers)})"