Skip to content

Commit cd5f89a

Browse files
committed
Centralize and standardize callback usage (#291)
* Centralize and standardize callback usage * Fix tests * Fix tests
1 parent b5ecaeb commit cd5f89a

7 files changed

Lines changed: 96 additions & 93 deletions

File tree

docs/api.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ Systems
7777
Utilities
7878
---------
7979

80+
.. automodule:: simplipy.util
81+
:members:
82+
8083
``auth``
8184
********
8285

docs/websocket.rst

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@ Disconnecting
3333
Responding to Events
3434
--------------------
3535

36-
Users respond to events by defining listeners (synchronous functions *or* coroutines).
36+
Users respond to events by defining callbacks (synchronous functions *or* coroutines).
3737
The following events exist:
3838

3939
* ``connect``: occurs when the websocket connection is established
4040
* ``disconnect``: occurs when the websocket connection is terminated
4141
* ``event``: occurs when any data is transmitted from the SimpliSafe™ cloud
4242

43-
Note that you can register as many listeners as you'd like.
43+
Note that you can register as many callbacks as you'd like.
4444

4545
``connect``
4646
***********
@@ -54,10 +54,10 @@ Note that you can register as many listeners as you'd like.
5454
def connect_handler():
5555
print("I connected to the websocket")
5656
57-
remove_1 = simplisafe.websocket.add_connect_listener(async_connect_handler)
58-
remove_2 = simplisafe.websocket.add_connect_listener(connect_handler)
57+
remove_1 = simplisafe.websocket.add_connect_callback(async_connect_handler)
58+
remove_2 = simplisafe.websocket.add_connect_callback(connect_handler)
5959
60-
# remove_1 and remove_2 are functions that, when called, remove the listener.
60+
# remove_1 and remove_2 are functions that, when called, remove the callback.
6161
6262
``disconnect``
6363
**************
@@ -71,10 +71,10 @@ Note that you can register as many listeners as you'd like.
7171
def connect_handler():
7272
print("I disconnected from the websocket")
7373
74-
remove_1 = simplisafe.websocket.add_disconnect_listener(async_connect_handler)
75-
remove_2 = simplisafe.websocket.add_disconnect_listener(connect_handler)
74+
remove_1 = simplisafe.websocket.add_disconnect_callback(async_connect_handler)
75+
remove_2 = simplisafe.websocket.add_disconnect_callback(connect_handler)
7676
77-
# remove_1 and remove_2 are functions that, when called, remove the listener.
77+
# remove_1 and remove_2 are functions that, when called, remove the callback.
7878
7979
``event``
8080
*********
@@ -88,15 +88,15 @@ Note that you can register as many listeners as you'd like.
8888
def connect_handler():
8989
print(f"I received a SimpliSafe™ event: {event}")
9090
91-
remove_1 = simplisafe.websocket.add_event_listener(async_connect_handler)
92-
remove_2 = simplisafe.websocket.add_event_listener(connect_handler)
91+
remove_1 = simplisafe.websocket.add_event_callback(async_connect_handler)
92+
remove_2 = simplisafe.websocket.add_event_callback(connect_handler)
9393
94-
# remove_1 and remove_2 are functions that, when called, remove the listener.
94+
# remove_1 and remove_2 are functions that, when called, remove the callback.
9595
9696
Response Format
9797
===============
9898

99-
The ``event`` argument provided to event listeners is a
99+
The ``event`` argument provided to event callbacks is a
100100
:meth:`simplipy.websocket.WebsocketEvent` object, which comes with several properties:
101101

102102
* ``changed_by``: the PIN that caused the event (in the case of arming/disarming/etc.)

simplipy/api.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
)
2020
from simplipy.system.v2 import SystemV2
2121
from simplipy.system.v3 import SystemV3
22+
from simplipy.util import schedule_callback
2223
from simplipy.util.auth import (
2324
AUTH_URL_BASE,
2425
AUTH_URL_HOSTNAME,
@@ -65,7 +66,7 @@ def __init__(
6566
request_retries: int = DEFAULT_REQUEST_RETRIES,
6667
) -> None:
6768
"""Initialize."""
68-
self._refresh_token_listeners: list[Callable[..., None]] = []
69+
self._refresh_token_callbacks: list[Callable[..., None]] = []
6970
self.session: ClientSession = session
7071

7172
# These will get filled in after initial authentication:
@@ -215,8 +216,8 @@ async def _async_refresh_access_token(self) -> None:
215216
self.access_token = token_resp["access_token"]
216217
self.refresh_token = token_resp["refresh_token"]
217218

218-
for callback in self._refresh_token_listeners:
219-
callback(self.refresh_token)
219+
for callback in self._refresh_token_callbacks:
220+
schedule_callback(callback, self.refresh_token)
220221

221222
async def _async_request(
222223
self, method: str, endpoint: str, url_base: str = API_URL_BASE, **kwargs: Any
@@ -261,21 +262,21 @@ async def _async_request(
261262

262263
return data
263264

264-
def add_refresh_token_listener(
265+
def add_refresh_token_callback(
265266
self, callback: Callable[..., None]
266267
) -> Callable[..., None]:
267-
"""Add a listener that should be triggered when tokens are refreshed.
268+
"""Add a callback that should be triggered when tokens are refreshed.
268269
269270
Note that callbacks should expect to receive a refresh token as a parameter.
270271
271272
:param callback: The method to call after receiving an event.
272273
:type callback: ``Callable[..., None]``
273274
"""
274-
self._refresh_token_listeners.append(callback)
275+
self._refresh_token_callbacks.append(callback)
275276

276277
def remove() -> None:
277278
"""Remove the callback."""
278-
self._refresh_token_listeners.remove(callback)
279+
self._refresh_token_callbacks.remove(callback)
279280

280281
return remove
281282

simplipy/util/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,12 @@
11
"""Define utility modules."""
2+
import asyncio
3+
from typing import Any, Callable
4+
5+
6+
def schedule_callback(callback: Callable[..., Any], *args: Any) -> None:
7+
"""Schedule a callback to be called."""
8+
if asyncio.iscoroutinefunction(callback):
9+
asyncio.create_task(callback(*args))
10+
else:
11+
loop = asyncio.get_running_loop()
12+
loop.call_soon(callback, *args)

simplipy/websocket.py

Lines changed: 27 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
InvalidMessageError,
2323
NotConnectedError,
2424
)
25+
from simplipy.util import schedule_callback
2526
from simplipy.util.dt import utc_from_timestamp
2627

2728
if TYPE_CHECKING:
@@ -116,11 +117,7 @@ def __init__(
116117
def _on_expire(self) -> None:
117118
"""Log and act when the watchdog expires."""
118119
LOGGER.info("Websocket watchdog expired")
119-
120-
if asyncio.iscoroutinefunction(self._action):
121-
asyncio.create_task(self._action())
122-
else:
123-
self._loop.call_soon(self._action())
120+
schedule_callback(self._action)
124121

125122
def cancel(self) -> None:
126123
"""Cancel the watchdog."""
@@ -213,9 +210,9 @@ class WebsocketClient:
213210
def __init__(self, api: API) -> None:
214211
"""Initialize."""
215212
self._api = api
216-
self._connect_listeners: list[Callable[..., None]] = []
217-
self._disconnect_listeners: list[Callable[..., None]] = []
218-
self._event_listeners: list[Callable[..., None]] = []
213+
self._connect_callbacks: list[Callable[..., None]] = []
214+
self._disconnect_callbacks: list[Callable[..., None]] = []
215+
self._event_callbacks: list[Callable[..., None]] = []
219216
self._loop = asyncio.get_running_loop()
220217
self._watchdog = Watchdog(self.async_reconnect)
221218

@@ -228,15 +225,15 @@ def connected(self) -> bool:
228225
return self._client is not None and not self._client.closed
229226

230227
@staticmethod
231-
def _add_listener(
232-
listener_list: list, callback: Callable[..., Any]
228+
def _add_callback(
229+
callback_list: list, callback: Callable[..., Any]
233230
) -> Callable[..., None]:
234-
"""Add a listener callback to a particular list."""
235-
listener_list.append(callback)
231+
"""Add a callback callback to a particular list."""
232+
callback_list.append(callback)
236233

237234
def remove() -> None:
238235
"""Remove the callback."""
239-
listener_list.remove(callback)
236+
callback_list.remove(callback)
240237

241238
return remove
242239

@@ -283,44 +280,37 @@ def _parse_message(self, message: dict[str, Any]) -> None:
283280
"""Parse an incoming message."""
284281
if message["type"] == "com.simplisafe.event.standard":
285282
event = websocket_event_from_payload(message)
286-
for listener in self._event_listeners:
287-
self._schedule_listener_call(listener, event)
288-
289-
def _schedule_listener_call(self, listener: Callable[..., Any], *args: Any) -> None:
290-
"""Schedule a listener to be called."""
291-
if asyncio.iscoroutinefunction(listener):
292-
asyncio.create_task(listener(*args))
293-
else:
294-
self._loop.call_soon(listener, *args)
283+
for callback in self._event_callbacks:
284+
schedule_callback(callback, event)
295285

296-
def add_connect_listener(self, callback: Callable[..., Any]) -> Callable[..., None]:
297-
"""Add a listener callback to be called after connecting.
286+
def add_connect_callback(self, callback: Callable[..., Any]) -> Callable[..., None]:
287+
"""Add a callback callback to be called after connecting.
298288
299289
:param callback: The method to call after connecting
300290
:type callback: ``Callable[..., None]``
301291
"""
302-
return self._add_listener(self._connect_listeners, callback)
292+
return self._add_callback(self._connect_callbacks, callback)
303293

304-
def add_disconnect_listener(
294+
def add_disconnect_callback(
305295
self, callback: Callable[..., Any]
306296
) -> Callable[..., None]:
307-
"""Add a listener callback to be called after disconnecting.
297+
"""Add a callback callback to be called after disconnecting.
308298
309299
:param callback: The method to call after disconnecting
310300
:type callback: ``Callable[..., None]``
311301
"""
312-
return self._add_listener(self._disconnect_listeners, callback)
302+
return self._add_callback(self._disconnect_callbacks, callback)
313303

314-
def add_event_listener(self, callback: Callable[..., Any]) -> Callable[..., None]:
315-
"""Add a listener callback to be called upon receiving an event.
304+
def add_event_callback(self, callback: Callable[..., Any]) -> Callable[..., None]:
305+
"""Add a callback callback to be called upon receiving an event.
316306
317307
Note that callbacks should expect to receive a WebsocketEvent object as a
318308
parameter.
319309
320310
:param callback: The method to call after receiving an event.
321311
:type callback: ``Callable[..., None]``
322312
"""
323-
return self._add_listener(self._event_listeners, callback)
313+
return self._add_callback(self._event_callbacks, callback)
324314

325315
async def async_connect(self) -> None:
326316
"""Connect to the websocket server."""
@@ -333,8 +323,8 @@ async def async_connect(self) -> None:
333323

334324
LOGGER.info("Connected to websocket server")
335325

336-
for listener in self._connect_listeners:
337-
self._schedule_listener_call(listener)
326+
for callback in self._connect_callbacks:
327+
schedule_callback(callback)
338328

339329
self._watchdog.trigger()
340330

@@ -346,8 +336,8 @@ async def async_disconnect(self) -> None:
346336

347337
LOGGER.info("Disconnected from websocket server")
348338

349-
for listener in self._disconnect_listeners:
350-
self._schedule_listener_call(listener)
339+
for callback in self._disconnect_callbacks:
340+
schedule_callback(callback)
351341

352342
async def async_listen(self) -> None:
353343
"""Start listening to the websocket server."""
@@ -384,8 +374,8 @@ async def async_listen(self) -> None:
384374
finally:
385375
LOGGER.debug("Listen completed; cleaning up")
386376

387-
for listener in self._disconnect_listeners:
388-
self._schedule_listener_call(listener)
377+
for callback in self._disconnect_callbacks:
378+
schedule_callback(callback)
389379

390380
async def async_reconnect(self) -> None:
391381
"""Reconnect (and re-listen, if appropriate) to the websocket."""

tests/test_api.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Define tests for the System object."""
22
# pylint: disable=protected-access,too-many-arguments
3+
import asyncio
34
from datetime import datetime
45
from unittest.mock import Mock
56

@@ -205,18 +206,14 @@ async def test_client_async_from_refresh_token(
205206

206207

207208
@pytest.mark.asyncio
208-
async def test_refresh_token_listener_callback(
209+
async def test_refresh_token_callback(
209210
api_token_response,
210211
aresponses,
211-
caplog,
212212
server,
213213
v2_settings_response,
214214
v2_subscriptions_response,
215215
):
216-
"""Test that listener callbacks are executed correctly."""
217-
import logging
218-
219-
caplog.set_level(logging.DEBUG)
216+
"""Test that callback callbacks are executed correctly."""
220217
server.add(
221218
"api.simplisafe.com",
222219
f"/v1/users/{TEST_SUBSCRIPTION_ID}/subscriptions",
@@ -254,8 +251,8 @@ async def test_refresh_token_listener_callback(
254251
response=aiohttp.web_response.json_response(v2_settings_response, status=200),
255252
)
256253

257-
mock_listener_1 = Mock()
258-
mock_listener_2 = Mock()
254+
mock_callback_1 = Mock()
255+
mock_callback_2 = Mock()
259256

260257
async with aiohttp.ClientSession() as session:
261258
simplisafe = await API.async_from_auth(
@@ -265,18 +262,19 @@ async def test_refresh_token_listener_callback(
265262
# Manually set the expiration datetime to force a refresh token flow:
266263
simplisafe._access_token_expire_dt = datetime.utcnow()
267264

268-
# We'll hang onto one listener callback:
269-
simplisafe.add_refresh_token_listener(mock_listener_1)
270-
assert mock_listener_1.call_count == 0
265+
# We'll hang onto one callback callback:
266+
simplisafe.add_refresh_token_callback(mock_callback_1)
267+
assert mock_callback_1.call_count == 0
271268

272269
# ..and delete the a second one before ever using it:
273-
remove = simplisafe.add_refresh_token_listener(mock_listener_2)
270+
remove = simplisafe.add_refresh_token_callback(mock_callback_2)
274271
remove()
275272

276273
await simplisafe.async_get_systems()
277-
mock_listener_1.assert_called_once_with("aabbcc11")
278-
assert mock_listener_1.call_count == 1
279-
assert mock_listener_2.call_count == 0
274+
await asyncio.sleep(1)
275+
mock_callback_1.assert_called_once_with("aabbcc11")
276+
assert mock_callback_1.call_count == 1
277+
assert mock_callback_2.call_count == 0
280278

281279

282280
@pytest.mark.asyncio

0 commit comments

Comments
 (0)