Skip to content

Commit b60a344

Browse files
committed
Remove ability to trap exceptions from callbacks
At most, such exceptions will now be logged.
1 parent 18e5614 commit b60a344

7 files changed

Lines changed: 20 additions & 78 deletions

File tree

‎src/powersensor_local/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
'Events',
5252
'Message',
5353
]
54-
__version__ = "2.4.0rc2"
54+
__version__ = "2.4.0rc3"
5555
from .devices import PowersensorDevices, PowersensorLegacyDevices
5656
from .legacy_discovery import LegacyDiscovery
5757
from .plug_api import PlugApi

‎src/powersensor_local/async_event_emitter.py‎

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,13 @@ async def emit(self, event_name: str, *args: Any) -> None:
3131
"""Emits an event to all registered listeners for that event type.
3232
Additional arguments may be supplied with event as appropriate. Each
3333
event handler is awaited before delivering the event to the next.
34-
If an event handler raises an exception, this is funneled through
35-
to an 'exception' event being emitted. If no 'exception' listener
36-
is registered, or an exception handler callback raises an exception,
37-
the exception is logged (if a logger was provided), and discarded."""
34+
If an event handler raises an exception it is logged (if a logger was
35+
provided), and discarded."""
3836
if self._listeners.get(event_name) is None:
3937
return
4038
for callback in self._listeners[event_name]:
4139
try:
4240
await callback(event_name, *args)
4341
except Exception as e:
44-
if 'exception' not in self._listeners:
4542
if self._logger is not None:
46-
self._logger.exception(f"Discarding unhandled exception: {e}")
47-
else:
48-
for handler in self._listeners['exception']:
49-
try:
50-
await handler('exception', e)
51-
except Exception as e2:
52-
if self._logger is not None:
53-
self._logger.exception(f"Exception handling callback raised an exception itself, discarding it: {e2}")
43+
self._logger.exception(f"Logic error: exception escaped from callback: {e}")

‎src/powersensor_local/devices.py‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
'average_power',
2222
'average_power_components',
2323
'battery_level',
24-
'exception',
2524
'now_relaying_for',
2625
'radio_signal_quality',
2726
'summation_energy',
@@ -73,7 +72,7 @@ class _PowersensorDevicesBase:
7372
``event`` field. Known measurement events include:
7473
7574
``average_flow``, ``average_power``, ``average_power_components``,
76-
``battery_level``, ``exception``, ``now_relaying_for``,
75+
``battery_level``, ``now_relaying_for``,
7776
``radio_signal_quality``, ``summation_energy``, ``summation_volume``.
7877
7978
When ``relay_now_relaying_for=True`` the raw ``now_relaying_for`` wire
@@ -187,7 +186,7 @@ async def _plug_discovered(self, mac: str, ip: str, port: int) -> None:
187186
await self._remove_device(mac)
188187

189188
await self._add_device(mac, 'plug')
190-
api = PlugApi(mac, ip, port)
189+
api = PlugApi(mac, ip, port, 'udp', self._logger)
191190
self._plug_apis[mac] = api
192191
for event in _KNOWN_PLUG_EVENTS:
193192
api.subscribe(event, self._reemit)

‎src/powersensor_local/plug_api.py‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Interface abstraction for Powersensor plugs."""
22
import sys
33

4+
from logging import Logger
5+
46
from .async_event_emitter import AsyncEventEmitter
57
from .plug_listener_tcp import PlugListenerTcp
68
from .plug_listener_udp import PlugListenerUdp
@@ -17,7 +19,7 @@ class PlugApi(AsyncEventEmitter):
1719
documented in xlatemsg.translate_raw_message.
1820
"""
1921

20-
def __init__(self, mac: str, ip: str, port: int = 49476, proto: str = 'udp'):
22+
def __init__(self, mac: str, ip: str, port: int = 49476, proto: str = 'udp', logger: Logger | None = None):
2123
"""Create a :class:`PlugApi` instance for a single plug.
2224
2325
Parameters
@@ -32,13 +34,15 @@ def __init__(self, mac: str, ip: str, port: int = 49476, proto: str = 'udp'):
3234
Protocol used for communication. ``'udp'`` selects :class:`PlugListenerUdp`,
3335
while ``'tcp'`` selects :class:`PlugListenerTcp`. Any other value raises a
3436
:class:`ValueError`.
37+
logger : Logger, optional
38+
If provided, enables the logging of escaped exceptions from callbacks.
3539
3640
Raises
3741
------
3842
ValueError
3943
If *proto* is not ``'udp'`` or ``'tcp'``.
4044
"""
41-
super().__init__()
45+
super().__init__(logger)
4246
self._mac: str = mac
4347
self._listener: PlugListenerUdp | PlugListenerTcp
4448
if proto == 'udp':
@@ -48,7 +52,6 @@ def __init__(self, mac: str, ip: str, port: int = 49476, proto: str = 'udp'):
4852
else:
4953
raise ValueError(f'Unsupported proto: {proto}')
5054
self._listener.subscribe('message', self._on_message)
51-
self._listener.subscribe('exception', self._on_exception)
5255
self._seen: set[str] = set()
5356

5457
def connect(self) -> None:
@@ -93,10 +96,6 @@ async def _on_message(self, _: str, message: Message) -> None:
9396
for name, ev in evs.items():
9497
await self.emit(name, ev)
9598

96-
async def _on_exception(self, _: str, e: Exception) -> None:
97-
"""Propagates exceptions from the plug listener."""
98-
await self.emit('exception', e)
99-
10099
@property
101100
def ip_address(self) -> str:
102101
"""

‎src/powersensor_local/plugevents.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""Utility script for accessing the plug api from a single network-local
44
Powersensor device. Intended for advanced debugging use only."""
55

6+
import logging
67
import sys
78
from pathlib import Path
89

@@ -15,6 +16,8 @@
1516
from powersensor_local.abstract_event_handler import AbstractEventHandler
1617
from powersensor_local.xlatemsg import Message
1718

19+
LOGGER = logging.getLogger(__name__)
20+
1821
async def print_event_and_message(event: str, message: Message) -> None:
1922
"""Callback for printing event data."""
2023
print(event, message)
@@ -37,9 +40,11 @@ async def main(self) -> None:
3740
# Signal handler for Ctrl+C
3841
self.register_sigint_handler()
3942

40-
plug = PlugApi(sys.argv[1], sys.argv[2], int(*sys.argv[3:3]))
43+
port = int(*sys.argv[3:3])
44+
if port == 0:
45+
port = 49476
46+
plug = PlugApi(sys.argv[1], sys.argv[2], port, 'udp', LOGGER)
4147
known_evs = [
42-
'exception',
4348
'average_flow',
4449
'average_power',
4550
'average_power_components',

‎src/powersensor_local/rawplug.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ async def main(self) -> None:
5959
else:
6060
print('Unsupported protocol:', self._protocol)
6161
sys.exit(1)
62-
self.plug.subscribe('exception', print_message_ignore_event)
6362
self.plug.subscribe('message', print_message_ignore_event)
6463
self.plug.subscribe('connecting', print_event)
6564
self.plug.subscribe('connecting', print_event)

‎tests/test_async_event_emitter.py‎

Lines changed: 1 addition & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -71,54 +71,4 @@ async def test_exception_unhandled() -> None:
7171
mock.side_effect = KeyError('oops')
7272
await emitter.emit('e')
7373
mock.assert_called_once()
74-
logger.exception.assert_called_once_with("Discarding unhandled exception: 'oops'")
75-
76-
77-
@pytest.mark.asyncio
78-
async def test_exception_handler(emitter: AsyncEventEmitter) -> None:
79-
mock = AsyncMock()
80-
emitter.subscribe('e', mock)
81-
e = KeyError('oops')
82-
mock.side_effect = e
83-
mock_exc = AsyncMock()
84-
emitter.subscribe('exception', mock_exc)
85-
await emitter.emit('e')
86-
mock.assert_called_once()
87-
mock_exc.assert_called_once_with('exception', e)
88-
89-
90-
@pytest.mark.asyncio
91-
async def test_exception_handler_exception() -> None:
92-
logger = MagicMock()
93-
emitter = AsyncEventEmitter(logger)
94-
mock = AsyncMock()
95-
emitter.subscribe('e', mock)
96-
mock.side_effect = KeyError('oops')
97-
mock_exc = AsyncMock()
98-
emitter.subscribe('exception', mock_exc)
99-
mock_exc.side_effect = ValueError('doh')
100-
await emitter.emit('e')
101-
mock.assert_called_once()
102-
mock_exc.assert_called_once()
103-
logger.exception.assert_called_once_with("Exception handling callback raised an exception itself, discarding it: doh")
104-
105-
106-
@pytest.mark.asyncio
107-
async def test_multiple_exception_handlers() -> None:
108-
logger = MagicMock()
109-
emitter = AsyncEventEmitter(logger)
110-
trigger = AsyncMock()
111-
e = ValueError('overflow')
112-
trigger.side_effect = e
113-
emitter.subscribe('e', trigger)
114-
handlers = [ AsyncMock() for _ in range(5) ]
115-
for handler in handlers:
116-
emitter.subscribe('exception', handler)
117-
bad_handlers = handlers[1::2] # pick every other
118-
for handler in bad_handlers:
119-
handler.side_effect = e
120-
await emitter.emit('e')
121-
trigger.assert_called_once()
122-
for handler in handlers:
123-
handler.assert_called_once()
124-
assert(logger.exception.call_count == len(bad_handlers))
74+
logger.exception.assert_called_once_with("Logic error: exception escaped from callback: 'oops'")

0 commit comments

Comments
 (0)