diff --git a/tests/api/test_listeners.py b/tests/api/test_listeners.py index 0485d211..617eb54d 100644 --- a/tests/api/test_listeners.py +++ b/tests/api/test_listeners.py @@ -6,6 +6,7 @@ import zigpy_znp.types as t import zigpy_znp.commands as c from zigpy_znp.api import OneShotResponseListener, CallbackResponseListener +from zigpy_znp.exceptions import ShuttingDown async def test_resolve(event_loop, mocker): @@ -34,24 +35,24 @@ async def test_resolve(event_loop, mocker): assert (await future) == match # Cancelling a callback will have no effect - assert not callback_listener.cancel() + callback_listener.set_exception(RuntimeError()) # Cancelling a one-shot listener does not throw any errors - assert one_shot_listener.cancel() - assert one_shot_listener.cancel() - assert one_shot_listener.cancel() + one_shot_listener.set_exception(RuntimeError()) + one_shot_listener.set_exception(RuntimeError()) + one_shot_listener.set_exception(RuntimeError()) async def test_cancel(event_loop): # Cancelling a one-shot listener prevents it from being fired future = event_loop.create_future() one_shot_listener = OneShotResponseListener([c.SYS.Ping.Rsp(partial=True)], future) - one_shot_listener.cancel() + one_shot_listener.set_exception(RuntimeError()) match = c.SYS.Ping.Rsp(Capabilities=t.MTCapabilities.SYS) assert not one_shot_listener.resolve(match) - with pytest.raises(asyncio.CancelledError): + with pytest.raises(RuntimeError): await future @@ -95,7 +96,7 @@ async def test_api_cancel_listeners(connected_znp, mocker): assert not future.done() znp.close() - with pytest.raises(asyncio.CancelledError): + with pytest.raises(ShuttingDown): await future # add_done_callback won't be executed immediately diff --git a/zigpy_znp/api.py b/zigpy_znp/api.py index 8c7d28b3..274e42fa 100644 --- a/zigpy_znp/api.py +++ b/zigpy_znp/api.py @@ -32,7 +32,11 @@ CallbackResponseListener, ) from zigpy_znp.frames import GeneralFrame -from zigpy_znp.exceptions import CommandNotRecognized, InvalidCommandResponse +from zigpy_znp.exceptions import ( + ShuttingDown, + CommandNotRecognized, + InvalidCommandResponse, +) from zigpy_znp.types.nvids import ExNvIds, OsalNvIds if typing.TYPE_CHECKING: @@ -774,7 +778,7 @@ def close(self) -> None: for _header, listeners in self._listeners.items(): for listener in listeners: - listener.cancel() + listener.set_exception(ShuttingDown()) self._listeners.clear() self.version = None diff --git a/zigpy_znp/exceptions.py b/zigpy_znp/exceptions.py index 2c61502f..d5d37a08 100644 --- a/zigpy_znp/exceptions.py +++ b/zigpy_znp/exceptions.py @@ -1,4 +1,4 @@ -from zigpy.exceptions import DeliveryError +from zigpy.exceptions import DeliveryError, ControllerException class InvalidFrame(ValueError): @@ -17,3 +17,7 @@ class InvalidCommandResponse(DeliveryError): def __init__(self, message, response): super().__init__(message) self.response = response + + +class ShuttingDown(ControllerException): + pass diff --git a/zigpy_znp/utils.py b/zigpy_znp/utils.py index 04d05ef1..475ada25 100644 --- a/zigpy_znp/utils.py +++ b/zigpy_znp/utils.py @@ -86,7 +86,7 @@ def _resolve(self, response: t.CommandBase) -> bool: raise NotImplementedError() # pragma: no cover - def cancel(self): + def set_exception(self, exc: BaseException) -> None: """ Implement by subclasses to cancel the listener. @@ -118,11 +118,9 @@ def _resolve(self, response: t.CommandBase) -> bool: self.future.set_result(response) return True - def cancel(self): + def set_exception(self, exc: BaseException) -> None: if not self.future.done(): - self.future.cancel() - - return True + self.future.set_exception(exc) @dataclasses.dataclass(frozen=True) @@ -149,9 +147,9 @@ def _resolve(self, response: t.CommandBase) -> bool: # Callbacks are always resolved return True - def cancel(self): + def set_exception(self, exc: BaseException) -> None: # You can't cancel a callback - return False + pass class CatchAllResponse: