python-can icon indicating copy to clipboard operation
python-can copied to clipboard

Async Listener

Open Adirio opened this issue 2 years ago • 2 comments

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:

  1. Creating a new AsyncListener with the above version of __call__, and async signatures for on_message_received and on_error.
  2. Updating the Listener class 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.

Adirio avatar Jun 29 '23 14:06 Adirio

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!

IngmarPaetzold avatar Aug 01 '24 15:08 IngmarPaetzold

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.

sveinse avatar Jan 20 '25 21:01 sveinse