decorators.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. """
  2. Event handling decorators for the Trixy application.
  3. This module provides the @TrixyEvent decorator that allows methods to be
  4. registered as event handlers for specific event types. The decorator handles
  5. automatic registration with the EventHandler and provides validation.
  6. """
  7. import functools
  8. import inspect
  9. from typing import Callable, List, Union, Any, Type, get_type_hints
  10. from .event_data import EventType, TrixyEventData
  11. def pprint(message: str) -> None:
  12. """
  13. Debug printing function that adapts based on mode.
  14. In production: uses proper logging, in debug: uses print.
  15. """
  16. # TODO: This should integrate with the application's logging system
  17. # For now, we'll use print but this should be replaced with proper logging
  18. print(f"[TrixyEvent] {message}")
  19. class TrixyEventRegistry:
  20. """
  21. Registry for tracking all @TrixyEvent decorated methods.
  22. This class maintains a mapping of event types to their registered handlers
  23. and provides methods for registration and retrieval of handlers.
  24. """
  25. def __init__(self):
  26. self._handlers: dict[str, List[Callable]] = {}
  27. self._handler_metadata: dict[Callable, dict] = {}
  28. def register_handler(self, event_types: List[str], handler: Callable,
  29. priority: int = 0, async_handler: bool = False) -> None:
  30. """
  31. Register an event handler for the specified event types.
  32. Args:
  33. event_types: List of event type strings to handle
  34. handler: The handler function/method
  35. priority: Handler priority (higher = called first)
  36. async_handler: Whether the handler is async
  37. """
  38. for event_type in event_types:
  39. if event_type not in self._handlers:
  40. self._handlers[event_type] = []
  41. if handler not in self._handlers[event_type]:
  42. self._handlers[event_type].append(handler)
  43. # Store metadata
  44. self._handler_metadata[handler] = {
  45. 'event_types': event_types,
  46. 'priority': priority,
  47. 'async_handler': async_handler,
  48. 'class_name': getattr(handler, '__qualname__', handler.__name__),
  49. 'module_name': getattr(handler, '__module__', 'unknown')
  50. }
  51. # Sort handlers by priority (higher priority first)
  52. self._handlers[event_type].sort(
  53. key=lambda h: self._handler_metadata.get(h, {}).get('priority', 0),
  54. reverse=True
  55. )
  56. pprint(f"Registered handler {handler.__name__} for events: {event_types}")
  57. def unregister_handler(self, handler: Callable) -> None:
  58. """
  59. Unregister an event handler from all event types.
  60. Args:
  61. handler: The handler function/method to unregister
  62. """
  63. if handler in self._handler_metadata:
  64. event_types = self._handler_metadata[handler]['event_types']
  65. for event_type in event_types:
  66. if event_type in self._handlers and handler in self._handlers[event_type]:
  67. self._handlers[event_type].remove(handler)
  68. # Clean up empty event type lists
  69. if not self._handlers[event_type]:
  70. del self._handlers[event_type]
  71. del self._handler_metadata[handler]
  72. pprint(f"Unregistered handler {handler.__name__}")
  73. def get_handlers(self, event_type: str) -> List[Callable]:
  74. """
  75. Get all handlers for a specific event type.
  76. Args:
  77. event_type: The event type string
  78. Returns:
  79. List of handler functions, sorted by priority
  80. """
  81. return self._handlers.get(event_type, []).copy()
  82. def get_all_handlers(self) -> dict[str, List[Callable]]:
  83. """Get all registered handlers grouped by event type."""
  84. return {k: v.copy() for k, v in self._handlers.items()}
  85. def get_handler_metadata(self, handler: Callable) -> dict:
  86. """Get metadata for a specific handler."""
  87. return self._handler_metadata.get(handler, {}).copy()
  88. def get_event_types(self) -> List[str]:
  89. """Get all event types that have registered handlers."""
  90. return list(self._handlers.keys())
  91. def clear(self) -> None:
  92. """Clear all registered handlers."""
  93. self._handlers.clear()
  94. self._handler_metadata.clear()
  95. pprint("Cleared all event handlers from registry")
  96. # Global registry instance
  97. _event_registry = TrixyEventRegistry()
  98. def get_event_registry() -> TrixyEventRegistry:
  99. """Get the global event registry instance."""
  100. return _event_registry
  101. def TrixyEvent(event_types: Union[str, List[str]], priority: int = 0, async_handler: bool = False):
  102. """
  103. Decorator for registering methods as Trixy event handlers.
  104. This decorator automatically registers the decorated method to handle
  105. the specified event types. The method will be called whenever those
  106. events are triggered through the EventHandler.
  107. Args:
  108. event_types: Single event type string or list of event type strings
  109. priority: Handler priority (0-100, higher = called first)
  110. async_handler: Set to True if the handler is an async function
  111. Example:
  112. @TrixyEvent(["wakeword_received", "text_received"])
  113. def handle_events(self, event_name, event_data):
  114. if event_name == "wakeword_received":
  115. # Handle wakeword
  116. pass
  117. elif event_name == "text_received":
  118. # Handle text
  119. pass
  120. @TrixyEvent("system_startup", priority=10)
  121. def on_startup(self, event_name, event_data):
  122. # High priority startup handler
  123. pass
  124. """
  125. def decorator(func: Callable) -> Callable:
  126. # Normalize event_types to list
  127. if isinstance(event_types, str):
  128. normalized_event_types = [event_types]
  129. else:
  130. normalized_event_types = list(event_types)
  131. # Validate event types
  132. for event_type in normalized_event_types:
  133. try:
  134. EventType(event_type)
  135. except ValueError:
  136. pprint(f"Warning: Unknown event type '{event_type}' in @TrixyEvent decorator")
  137. # Validate priority
  138. if not isinstance(priority, int) or priority < 0 or priority > 100:
  139. raise ValueError("Priority must be an integer between 0 and 100")
  140. # Validate function signature
  141. sig = inspect.signature(func)
  142. params = list(sig.parameters.keys())
  143. # Check if it's a method (has self parameter) or function
  144. if len(params) < 2:
  145. raise ValueError(
  146. f"@TrixyEvent decorated function {func.__name__} must have at least "
  147. "2 parameters: (event_name, event_data) or (self, event_name, event_data)"
  148. )
  149. # Store event information on the function for later registration
  150. if not hasattr(func, '_trixy_events'):
  151. func._trixy_events = []
  152. func._trixy_events.append({
  153. 'event_types': normalized_event_types,
  154. 'priority': priority,
  155. 'async_handler': async_handler
  156. })
  157. # Mark the function as a trixy event handler
  158. func._is_trixy_event_handler = True
  159. @functools.wraps(func)
  160. def wrapper(*args, **kwargs):
  161. return func(*args, **kwargs)
  162. # Copy the trixy event metadata to the wrapper
  163. wrapper._trixy_events = func._trixy_events
  164. wrapper._is_trixy_event_handler = True
  165. return wrapper
  166. return decorator
  167. def register_event_handlers(instance: Any) -> None:
  168. """
  169. Register all @TrixyEvent decorated methods from an instance.
  170. This function scans an object for methods decorated with @TrixyEvent
  171. and registers them with the global event registry.
  172. Args:
  173. instance: Object instance to scan for event handlers
  174. """
  175. class_name = instance.__class__.__name__
  176. registered_count = 0
  177. for method_name in dir(instance):
  178. method = getattr(instance, method_name)
  179. # Check if this is a bound method with trixy event decoration
  180. if (hasattr(method, '_is_trixy_event_handler') and
  181. hasattr(method, '_trixy_events')):
  182. for event_config in method._trixy_events:
  183. _event_registry.register_handler(
  184. event_types=event_config['event_types'],
  185. handler=method,
  186. priority=event_config['priority'],
  187. async_handler=event_config['async_handler']
  188. )
  189. registered_count += 1
  190. if registered_count > 0:
  191. pprint(f"Registered {registered_count} event handlers from {class_name}")
  192. def unregister_event_handlers(instance: Any) -> None:
  193. """
  194. Unregister all @TrixyEvent decorated methods from an instance.
  195. Args:
  196. instance: Object instance to unregister handlers from
  197. """
  198. class_name = instance.__class__.__name__
  199. unregistered_count = 0
  200. for method_name in dir(instance):
  201. method = getattr(instance, method_name)
  202. if (hasattr(method, '_is_trixy_event_handler') and
  203. hasattr(method, '_trixy_events')):
  204. _event_registry.unregister_handler(method)
  205. unregistered_count += 1
  206. if unregistered_count > 0:
  207. pprint(f"Unregistered {unregistered_count} event handlers from {class_name}")
  208. def validate_event_handler(func: Callable) -> bool:
  209. """
  210. Validate that a function has the correct signature for an event handler.
  211. Args:
  212. func: Function to validate
  213. Returns:
  214. bool: True if valid, False otherwise
  215. """
  216. try:
  217. sig = inspect.signature(func)
  218. params = list(sig.parameters.keys())
  219. # Should have at least 2 parameters: (event_name, event_data) or (self, event_name, event_data)
  220. if len(params) < 2:
  221. return False
  222. # Check if it has type hints for validation
  223. type_hints = get_type_hints(func)
  224. # Optional validation of type hints
  225. if len(params) >= 3: # Method with self
  226. event_name_param = params[1]
  227. event_data_param = params[2]
  228. else: # Function without self
  229. event_name_param = params[0]
  230. event_data_param = params[1]
  231. # Validate type hints if present
  232. if event_name_param in type_hints:
  233. if type_hints[event_name_param] != str:
  234. pprint(f"Warning: {func.__name__} event_name parameter should be typed as str")
  235. if event_data_param in type_hints:
  236. expected_type = type_hints[event_data_param]
  237. if not (expected_type == TrixyEventData or
  238. (hasattr(expected_type, '__origin__') and
  239. issubclass(expected_type.__origin__, TrixyEventData))):
  240. pprint(f"Warning: {func.__name__} event_data parameter should be typed as TrixyEventData")
  241. return True
  242. except Exception as e:
  243. pprint(f"Error validating event handler {func.__name__}: {e}")
  244. return False
  245. # Utility functions for debugging and introspection
  246. def list_registered_events() -> dict[str, List[str]]:
  247. """
  248. Get a summary of all registered events and their handlers.
  249. Returns:
  250. dict: Mapping of event types to handler names
  251. """
  252. result = {}
  253. for event_type, handlers in _event_registry.get_all_handlers().items():
  254. result[event_type] = [
  255. f"{h.__qualname__} (priority: {_event_registry.get_handler_metadata(h).get('priority', 0)})"
  256. for h in handlers
  257. ]
  258. return result
  259. def get_handler_info(handler: Callable) -> dict:
  260. """
  261. Get detailed information about a specific event handler.
  262. Args:
  263. handler: The handler function to get info for
  264. Returns:
  265. dict: Handler information including events, priority, etc.
  266. """
  267. return _event_registry.get_handler_metadata(handler)