Skip to content

Commit 35c01fa

Browse files
authored
Merge pull request #8 from strohganoff/fix/websocketclient-error-handling
WebsocketClient exception-handling
2 parents f869c2d + f68f1f5 commit 35c01fa

File tree

6 files changed

+39
-21
lines changed

6 files changed

+39
-21
lines changed

streamdeck/manager.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def run(self) -> None:
100100

101101
command_sender.send_action_registration(register_event=self._register_event, plugin_registration_uuid=self._registration_uuid)
102102

103-
for message in client.listen_forever():
103+
for message in client.listen():
104104
data: EventBase = event_adapter.validate_json(message)
105105

106106
if not is_valid_event_name(data.event):

streamdeck/websocket.py

+25-7
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
from logging import getLogger
55
from typing import TYPE_CHECKING
66

7-
from websockets.exceptions import ConnectionClosed, ConnectionClosedError, ConnectionClosedOK
7+
from websockets.exceptions import ConnectionClosed, ConnectionClosedOK
88
from websockets.sync.client import ClientConnection, connect
99

1010

1111
if TYPE_CHECKING:
1212
from collections.abc import Generator
1313
from typing import Any
1414

15-
from typing_extensions import Self
15+
from typing_extensions import Self # noqa: UP035
1616

1717

1818
logger = getLogger("streamdeck.websocket")
@@ -38,10 +38,14 @@ def send_event(self, data: dict[str, Any]) -> None:
3838
Args:
3939
data (dict[str, Any]): The event data to send.
4040
"""
41+
if self._client is None:
42+
msg = "WebSocket connection not established yet."
43+
raise ValueError(msg)
44+
4145
data_str = json.dumps(data)
4246
self._client.send(message=data_str)
4347

44-
def listen_forever(self) -> Generator[str | bytes, Any, None]:
48+
def listen(self) -> Generator[str | bytes, Any, None]:
4549
"""Listen for messages from the WebSocket server indefinitely.
4650
4751
TODO: implement more concise error-handling.
@@ -55,20 +59,34 @@ def listen_forever(self) -> Generator[str | bytes, Any, None]:
5559
message: str | bytes = self._client.recv()
5660
yield message
5761

62+
except ConnectionClosedOK:
63+
logger.debug("Connection was closed normally, stopping the client.")
64+
65+
except ConnectionClosed:
66+
logger.exception("Connection was closed with an error.")
67+
5868
except Exception:
59-
logger.exception("Failed to receive messages from websocket server.")
69+
logger.exception("Failed to receive messages from websocket server due to unexpected error.")
70+
71+
def start(self) -> None:
72+
"""Start the connection to the websocket server."""
73+
self._client = connect(uri=f"ws://localhost:{self._port}")
74+
75+
def stop(self) -> None:
76+
"""Close the WebSocket connection, if open."""
77+
if self._client is not None:
78+
self._client.close()
6079

6180
def __enter__(self) -> Self:
6281
"""Start the connection to the websocket server.
6382
6483
Returns:
6584
Self: The WebSocketClient instance after connecting to the WebSocket server.
6685
"""
67-
self._client = connect(uri=f"ws://localhost:{self._port}")
86+
self.start()
6887
return self
6988

7089
def __exit__(self, *args, **kwargs) -> None:
7190
"""Close the WebSocket connection, if open."""
72-
if self._client is not None:
73-
self._client.close()
91+
self.stop()
7492

tests/plugin_manager/conftest.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ def plugin_manager(port_number: int, plugin_registration_uuid: str) -> PluginMan
3232
def patch_websocket_client(monkeypatch: pytest.MonkeyPatch) -> Mock:
3333
"""Fixture that uses pytest's MonkeyPatch to mock WebSocketClient for the PluginManager run method.
3434
35-
The mocked WebSocketClient can be given fake event messages to yield when listen_forever() is called:
36-
```patch_websocket_client.listen_forever.return_value = [fake_event_json1, fake_event_json2, ...]```
35+
The mocked WebSocketClient can be given fake event messages to yield when listen() is called:
36+
```patch_websocket_client.listen.return_value = [fake_event_json1, fake_event_json2, ...]```
3737
3838
Args:
3939
monkeypatch: pytest's monkeypatch fixture.

tests/plugin_manager/test_command_sender_binding.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,12 @@ def mock_websocket_client_with_fake_events(patch_websocket_client: Mock) -> tupl
6464
Returns:
6565
tuple: Mocked instance of WebSocketClient, and a list of fake event messages.
6666
"""
67-
# Create a list of fake event messages, and convert them to json strings to be passed back by the client.listen_forever() method.
67+
# Create a list of fake event messages, and convert them to json strings to be passed back by the client.listen() method.
6868
fake_event_messages: list[events.EventBase] = [
6969
KeyDownEventFactory.build(action="my-fake-action-uuid"),
7070
]
7171

72-
patch_websocket_client.listen_forever.return_value = [event.model_dump_json() for event in fake_event_messages]
72+
patch_websocket_client.listen.return_value = [event.model_dump_json() for event in fake_event_messages]
7373

7474
return patch_websocket_client, fake_event_messages
7575

tests/plugin_manager/test_plugin_manager.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ def mock_websocket_client_with_event(patch_websocket_client: Mock) -> tuple[Mock
2020
Returns:
2121
tuple: Mocked instance of WebSocketClient, and a fake DialRotateEvent.
2222
"""
23-
# Create a fake event message, and convert it to a json string to be passed back by the client.listen_forever() method.
23+
# Create a fake event message, and convert it to a json string to be passed back by the client.listen() method.
2424
fake_event_message: DialRotate = DialRotateEventFactory.build()
25-
patch_websocket_client.listen_forever.return_value = [fake_event_message.model_dump_json()]
25+
patch_websocket_client.listen.return_value = [fake_event_message.model_dump_json()]
2626

2727
return patch_websocket_client, fake_event_message
2828

@@ -91,11 +91,11 @@ def test_plugin_manager_process_event(
9191

9292
plugin_manager.run()
9393

94-
# First check that the WebSocketClient's listen_forever() method was called.
94+
# First check that the WebSocketClient's listen() method was called.
9595
# This has been stubbed to return the fake_event_message's json string.
96-
mock_websocket_client.listen_forever.assert_called_once()
96+
mock_websocket_client.listen.assert_called_once()
9797

98-
# Check that the event_adapter.validate_json method was called with the stub json string returned by listen_forever().
98+
# Check that the event_adapter.validate_json method was called with the stub json string returned by listen().
9999
spied_event_adapter_validate_json = cast(Mock, event_adapter.validate_json)
100100
spied_event_adapter_validate_json.assert_called_once_with(fake_event_message.model_dump_json())
101101
# Check that the validate_json method returns the same event type model as the fake_event_message.

tests/test_websocket.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ def test_send_event_serializes_and_sends(mock_connection: Mock, port_number: int
4747

4848

4949
@pytest.mark.usefixtures("patched_connect")
50-
def test_listen_forever_yields_messages(mock_connection: Mock, port_number: int):
51-
"""Test that listen_forever yields messages from the WebSocket connection."""
50+
def test_listen_yields_messages(mock_connection: Mock, port_number: int):
51+
"""Test that listen yields messages from the WebSocket connection."""
5252
# Set up the mocked connection to return messages until closing
5353
mock_connection.recv.side_effect = ["message1", b"message2", WebSocketException()]
5454

5555
with WebSocketClient(port=port_number) as client:
56-
messages = list(client.listen_forever())
56+
messages = list(client.listen())
5757

58-
assert messages == ["message1", b"message2"]
58+
assert messages == ["message1", b"message2"]

0 commit comments

Comments
 (0)