Async Listener
Issue description
Currently, Listener objects are asyncio-ready but are not truely async.
Let me first clarify that AsyncBufferedReader is not a truely-async listener, it is a sync listener (sync on_message_received and on_error methods) that exposes an async buffer through the get_message method. It is pretty useful for a lot of cases but this is not the target of this feature request. A truely-async listener would allow for async on_message_received and on_error definitions.
Disclaimer: I haven't inspected the whole code so I'm not sure if there is anything of the underlying architecture that makes this kind of truely-async listeners an issue.
And what do I mean by "asyncio-ready" in the first sentence? Notifier._on_message_available is called when a new message can be received. Notifier.listeners can either be Listener instances or direct callbacks. Either way they are called, and if they return a coroutine (and the notifier was passed a loop), it will scheduler the corresponding task:
https://github.com/hardbyte/python-can/blob/dc0ae68acb28f01a503e084718cfa2acf39d1e16/can/notifier.py#L134-L143
Async callbacks can be provided to make use of this feature, but Listener.__call__ never returns a coroutine:
https://github.com/hardbyte/python-can/blob/dc0ae68acb28f01a503e084718cfa2acf39d1e16/can/listener.py#L42-L43
Solutions
Returning the result of self.on_message_received instead of implicitly returning None would work for both versions:
def __call__(self, msg: Message):
return self.on_message_received(msg)
In the sync version, self.on_message_receive(msg) returns None, while the async version will return the coroutine, which is exactly the desired behavior. Regaring type-hinting of the result, Optional[asyncio.coroutine] works up to Python 3.9, not sure what should be the 3.10 one as asyncio.coroutine is no longer a thing.
I see two ways of providing a truely-async Listener:
- Creating a new
AsyncListenerwith the above version of__call__, and async signatures foron_message_receivedandon_error. - Updating the
Listenerclass with the above version of__call__.
Alternatives
Meanwhile, the user can always modify the __call__ method of the subclass of Listener to the one provided above and it will work. I still think this should be provided to the user and not expected from him to discover that he needs to change that magic method of the subclass.
I just had this issue that a pure async function did work as a listener, while a Listener class (derived) implementing async def on_message_received(self, msg): raised the warning RuntimeWarning: coroutine 'MyListener.on_message_received' was never awaited.
(I had not implemented the __call__ method before.)
Now that works like a charm. Thanks!
This is an old issue, so let's make a new attempt at it.
The low-level transport in Notifier._rx_thread() and Notifier._on_message_received() are able to handle coroutine callbacks. However, as you point out, the existing callbacks are only regular callbacks, and the ABC for class Listener only accepts regular on_message_received()
I would suggest we do both 1) and 2). No 1 is needed to get a clean typed annotated interface when using purely async. No 2 should be added to get the coroutine returned if the user changed on_message_received() changed to async.