beeware / toga

A Python native, OS native GUI toolkit.
https://toga.readthedocs.io/en/latest/
BSD 3-Clause "New" or "Revised" License
4.35k stars 671 forks source link

Allow `App.startup()` to be async #2857

Open rmartin16 opened 1 month ago

rmartin16 commented 1 month ago

What is the problem or limitation you are having?

An app's startup() method cannot be defined as async. Therefore, to take async actions at startup, users must factor out the async logic to run from the event loop.

Describe the solution you'd like

With the resolution of https://github.com/beeware/toga/issues/2834, Toga now ensures that the event loop is running on all platforms when startup() is invoked. Given this, the entire startup process can now support being async. Specifically, users would be able to define their startup() method as async.

I imagine to accomplish this, the current App._startup() method would need to be broken up. The invocation of App.startup() would be via asyncio.create_task(handler(self.startup)); then the remaining logic in App._startup() would be refactored in to a new method that is added as a callback for the Task for App.startup().

Describe alternatives you've considered

Users can factor out the async logic in to either on_running() or their own custom task.

If this idea is rejected, then Toga should error if startup() is async; currently, it errors becausemain_window isn't set which is likely to be confusing to users.

Additional context

No response

freakboy3742 commented 1 month ago

Conceptually, I agree it would be desirable to allow startup() to be async. However, I'm not sure it's actually possible to do so.

The problem will be that in order for it to be async, the app loop needs to be running; but on some platforms, in order to be running, it needs to be fully configured with a window. More experimentation is required :-)

I think the implementation could actually be slightly simpler than you suggest: if the internals of _startup() is an async method that then awaits startup() and on_running() if they're co-routines, invoking them directly if they're not, then _startup() itself just needs to create the task for that internal _startup() wrapper.

Regardless, I agree that if startup can't be async, we should raise an error rather than invoke and not await a co-routine.