diff --git a/features/steps/event_bus_steps.py b/features/steps/event_bus_steps.py index a414e2ed2..63d2d7e82 100644 --- a/features/steps/event_bus_steps.py +++ b/features/steps/event_bus_steps.py @@ -267,6 +267,7 @@ def step_protocol_stubs_callable(ctx: Context) -> None: event = _make_event("plan.created") obj.emit(event) obj.subscribe(EventType.PLAN_CREATED, lambda e: None) + obj.unsubscribe(EventType.PLAN_CREATED, lambda e: None) # --------------------------------------------------------------------------- diff --git a/src/cleveragents/infrastructure/events/logging_bus.py b/src/cleveragents/infrastructure/events/logging_bus.py index 307bf4ed3..316cca351 100644 --- a/src/cleveragents/infrastructure/events/logging_bus.py +++ b/src/cleveragents/infrastructure/events/logging_bus.py @@ -105,5 +105,38 @@ class LoggingEventBus: raise TypeError("handler must be callable") self._subscriptions.setdefault(event_type, []).append(handler) + def unsubscribe( + self, + event_type: EventType, + handler: Callable[[DomainEvent], None], + ) -> bool: + """Remove *handler* from events of *event_type*. + + Removes only the **first** occurrence of *handler* (in case it was + registered multiple times). Returns ``True`` if a matching handler + was found and removed, ``False`` otherwise. + + Args: + event_type: The :class:`EventType` whose handler list to modify. + handler: The previously-registered callable to remove. + + Returns: + ``True`` if a handler was removed; ``False`` if no match found. + """ + if not isinstance(event_type, EventType): + raise TypeError( + f"event_type must be an EventType, got {type(event_type).__name__!r}" + ) + if not callable(handler): + raise TypeError("handler must be callable") + handlers = self._subscriptions.get(event_type) + if handlers is None: + return False + try: + handlers.remove(handler) + return True + except ValueError: + return False + __all__ = ["LoggingEventBus"] diff --git a/src/cleveragents/infrastructure/events/protocol.py b/src/cleveragents/infrastructure/events/protocol.py index 2c3cae85f..f0bb251eb 100644 --- a/src/cleveragents/infrastructure/events/protocol.py +++ b/src/cleveragents/infrastructure/events/protocol.py @@ -49,5 +49,26 @@ class EventBus(Protocol): """ ... + def unsubscribe( + self, + event_type: EventType, + handler: Callable[[DomainEvent], None], + ) -> bool: + """Remove *handler* from the subscriber list for *event_type*. + + Removes only the **first** occurrence of *handler* in the list for + *event_type* (in case it was registered multiple times). Returns + ``True`` if a matching handler was found and removed, ``False`` + otherwise. + + Args: + event_type: The :class:`EventType` whose handler list to modify. + handler: The previously-registered callable to remove. + + Returns: + ``True`` if a handler was removed; ``False`` if no match found. + """ + ... + __all__ = ["EventBus"] diff --git a/src/cleveragents/infrastructure/events/reactive.py b/src/cleveragents/infrastructure/events/reactive.py index ec7c63194..e829f93f3 100644 --- a/src/cleveragents/infrastructure/events/reactive.py +++ b/src/cleveragents/infrastructure/events/reactive.py @@ -184,6 +184,41 @@ class ReactiveEventBus: """ return self._stream + def unsubscribe( + self, + event_type: EventType, + handler: Callable[[DomainEvent], None], + ) -> bool: + """Remove *handler* from events of *event_type*. + + Removes only the **first** occurrence of *handler* (in case it was + registered multiple times). Returns ``True`` if a matching handler + was found and removed, ``False`` otherwise. + + Args: + event_type: The :class:`EventType` whose handler list to modify. + handler: The previously-registered callable to remove. + + Returns: + ``True`` if a handler was removed; ``False`` if no match found. + """ + if not isinstance(event_type, EventType): + raise TypeError( + f"event_type must be an EventType, got {type(event_type).__name__!r}" + ) + if not callable(handler): + raise TypeError("handler must be callable") + handlers = self._subscriptions.get(event_type) + if handlers is None: + return False + try: + handlers.remove(handler) + return True + except ValueError: + # Handler was not found in list (duplicates registered but + # this specific reference is not present). + return False + def close(self) -> None: """Complete the RxPY stream and prevent further event emission.