Source code for ClearMap.Utils.event_bus

import functools
import inspect
import threading
import weakref
from types import FunctionType
from typing import Callable, Any, Dict, List, Optional, Type, TypeVar


Evt = TypeVar("Evt")
EventSubscriber = Callable[[Evt], None]


[docs] class Publishes: """ Marker interface for classes that publish events. This is just a nice helper for documentation and introspection. """ def __init__(self, *events: type): self.events = frozenset(events)
[docs] class EventBus: """Minimal, thread-safe publisher/subscriber for Events.""" def __init__(self) -> None: self._subs: Dict[Type[Any], List[weakref.ReferenceType]] = {} self._lock = threading.RLock() self._qt_bridge: Optional[Callable[[Type[Any], Any], None]] = None def _remove_refs(self, event_type: Type[Evt], refs_to_remove: List[weakref.ReferenceType]) -> None: if not refs_to_remove: return with self._lock: lst = self._subs.get(event_type, []) if not lst: return # refset = set(refs_to_remove) # lst[:] = [r for r in lst if r not in refset] lst[:] = [r for r in lst if r() is not None]
[docs] def subscribe(self, event_type: Type[Evt], callback: EventSubscriber[Evt]) -> Callable[[], None]: """ Subscribe a handler for a given event class. Handler signature: def handler(evt: EventType) -> None Returns: unsubscribe() callable. """ ref: weakref.ReferenceType # Methods use WeakMethod; functions use weakref.ref ref = weakref.WeakMethod(callback) if hasattr(callback, "__self__") \ else weakref.ref(callback) with self._lock: self._subs.setdefault(event_type, []).append(ref) def _unsub() -> None: self._remove_refs(event_type, [ref]) return _unsub
[docs] def publish(self, event: Evt) -> None: """ Publish a concrete event instance. Dispatch is on its exact class. (No inheritance walk for determinism/perf.) """ event_type = type(event) with self._lock: refs = list(self._subs.get(event_type, [])) dead: List[weakref.ReferenceType] = [] for ref in refs: cb = ref() if cb is None: dead.append(ref) continue # try: cb(event) # except Exception as err: # print(str(err)) # keep bus robust # TODO: log self._remove_refs(event_type, dead) if self._qt_bridge: self._qt_bridge(event_type, event)
[docs] def bridge_to_qt(self, forwarder: Callable[[Type[Any], Any], None]) -> None: """Optionally forward all events to a Qt slot/signal.""" self._qt_bridge = forwarder
[docs] class BusSubscriberMixin: def __init__(self, bus: EventBus): self._bus = bus self._unsubs: List[Callable[[], None]] = [] @staticmethod def _is_lambda(cb: Any) -> bool: return isinstance(cb, FunctionType) and cb.__name__ == "<lambda>" @staticmethod def _is_local_function(cb: Any) -> bool: # e.g. def inside __init__ / method body; fragile like lambda return isinstance(cb, FunctionType) and "<locals>" in getattr(cb, "__qualname__", "") @staticmethod def _is_partial(cb: Any) -> bool: return isinstance(cb, functools.partial) @classmethod def _is_partial_of_lambda_or_local(cls, cb: Any) -> bool: return cls._is_partial(cb) and (cls._is_lambda(cb.func) or cls._is_local_function(cb.func)) @staticmethod def _is_bound_method(cb: Any) -> bool: # Bound method: weakref.WeakMethod-safe and lifetime tied to self return inspect.ismethod(cb) and getattr(cb, "__self__", None) is not None @staticmethod def _is_module_level_function(cb: Any) -> bool: # Functions defined at module top-level have a strong ref via module dict return isinstance(cb, FunctionType) and "<locals>" not in getattr(cb, "__qualname__", "")
[docs] def subscribe(self, event_type: Type[Evt], handler: EventSubscriber[Evt], *, allow_unsafe: bool = False) -> None: unsafe = ( self._is_lambda(handler) or self._is_local_function(handler) or self._is_partial_of_lambda_or_local(handler) ) if unsafe and not allow_unsafe: raise TypeError( "Refusing to subscribe an unsafe handler (lambda/local function). " "These are weak-referenced by the EventBus and can be garbage-collected, " "silently breaking your subscription. Use a bound method (e.g. self.on_event) " "or a module-level function, or pass allow_unsafe=True if you really know " "what you’re doing." ) unsub = self._bus.subscribe(event_type, handler) self._unsubs.append(unsub)
[docs] def unsubscribe_all(self) -> None: for u in self._unsubs: try: u() except Exception: pass # TODO: log self._unsubs.clear()
[docs] def publish(self, event) -> None: self._bus.publish(event)