flet-dev / flet

Flet enables developers to easily build realtime web, mobile and desktop apps in Python. No frontend experience required.
https://flet.dev
Apache License 2.0
9.59k stars 372 forks source link

RFP: Async support in Flet Python client #128

Closed FeodorFitsner closed 1 year ago

FeodorFitsner commented 1 year ago

Discussed in https://github.com/flet-dev/flet/discussions/72

Problem

It is currently impossible to call async method from Flet event handlers. They are synchronous functions run in separate threads. This limitation requires to use bulky constructs like:

search_data  = new_event_loop().run_until_complete(get_weather(search_value))

where get_weather(search_value) is async function.

Solution

As in other languages like C# a "proper" async must be implemented from ground up - it's impossible/ineffective to partially support calling async methods from sync ones. The entire program must be running in an event loop (in Python terms).

Flet Python client must be re-implemented to use asyncio library.

Sample code

Flet client will be providing both sync and async methods, for example async version of "Hello, world" might look like:

import asyncio
import flet as ft

async def main(page: ft.Page):

    async def btn_click(e):
        var result = await some_call_to_rest_api()
        page.controls.append(ft.Text(result))
        await page.update_async()

    await page.add_async(
        ft.Text("Hello!"),
        ft.ElevatedButton("Call async method", on_click=btn_click)
    )

ft.app(target=main)

All async methods will have _async suffix.

WebSockets client will be implemented with websockets library based on asyncio.

Resources:

Flet API

Sync is going to be default style of writing Flet apps. Flet API that needs to be available in both sync and async variants:

mikaelho commented 1 year ago

Having asyncio support "natively" would be awesome -- but it does seem like a lot of work. Sprinkling async and await all over the place does make any code look unwieldy, and maintaining the sync alternatives seems like almost doubling the maintenance effort.

Anyone wanting to call into async functions from flet code could take advantage of the minimal unsync package, where a simple usage example would be:

import asyncio

import flet
from flet import ElevatedButton
from flet import Page
from unsync import unsync

def clicked(event):
    something_async()

@unsync
async def something_async():
    print("Starting an async operation")
    await asyncio.sleep(1)
    print("Finished")

def main(page: Page):
    page.add(ElevatedButton("Click me to do something async", on_click=clicked))
    page.update()

flet.app(target=main)

unsync maintains a singleton loop for its use, so the overhead is reasonable for most use cases.

TutorExilius commented 1 year ago

Is there a chance to support asyncio soon? When can we expect this support, is there a rough date for it?

FeodorFitsner commented 1 year ago

It's not a high priority right now. What would be your use case for that?

TutorExilius commented 1 year ago

Easy integration of projects working with asyncio.

Currently, to bring these projects together with flet, I would have to create a workaround (calling async functions). For now I use qt and set the qt main loop as my asyncio event loop, bringing the two worlds together,... which allows me to use the gui with async functions.

If I could use the event loop of flet as asynchio loop, it would be easier, something like this:


import asyncio
import sys

from asyncqt import QEventLoop
from PySide2.QtWidgets import QApplication

from pykalah.view.main_window import MainWindow

def main() -> None:
    """The main function"""

    app = QApplication(sys.argv)

    loop = QEventLoop(app) 
    asyncio.set_event_loop(loop)  #  <-bring qt and asyncio together

    window = MainWindow(None, initial_amount_pieces=6)
    window.show()

    with loop:
        sys.exit(loop.run_forever())

if __name__ == "__main__":
    main()```
FeodorFitsner commented 1 year ago

It looks great, of course, it's just we don't have enough hands at the moment to implement that 😕

FeodorFitsner commented 1 year ago

We go ahead with Async support in Flet: https://flet.dev/blog/flet-mobile-update#asyncio-support

TutorExilius commented 1 year ago

How wonderful, thanks! :)

FeodorFitsner commented 1 year ago

So, async support has been merged into main and you can give it a try by installing pre-release version of Flet package with pip install flet --pre.

Async version of "counter" app:

import flet as ft

async def main(page: ft.Page):
    page.title = "Flet counter example"
    page.vertical_alignment = ft.MainAxisAlignment.CENTER

    txt_number = ft.TextField(value="0", text_align=ft.TextAlign.RIGHT, width=100)

    async def minus_click(e):
        txt_number.value = str(int(txt_number.value) - 1)
        await page.update_async()

    async def plus_click(e):
        txt_number.value = str(int(txt_number.value) + 1)
        await page.update_async()

    await page.add_async(
        ft.Row(
            [
                ft.IconButton(ft.icons.REMOVE, on_click=minus_click),
                txt_number,
                ft.IconButton(ft.icons.ADD, on_click=plus_click),
            ],
            alignment=ft.MainAxisAlignment.CENTER,
        )
    )

ft.app(main)

A few async samples:

roniemartinez commented 1 year ago

Already tried this! Awesome implementation.

I think there is a bug with pubsub @FeodorFitsner

TypeError: __main__.main.<locals>.handle_message() argument after ** must be a mapping, not list

This line should only have 1 asterisk/star: https://github.com/flet-dev/flet/blob/14c215b0194ae94ebf87aef8efe7b1aaf6e1ee7e/sdk/python/flet/pubsub.py#L177

FeodorFitsner commented 1 year ago

Great, will take a look!

JartanFTW commented 1 year ago

This is my first time using Flet, so this could be normal but possibly not. Using the example code from the appbar documentation, when I close the Flet GUI, I get:

Traceback (most recent call last):
  File "C:\Users\jgogo\Desktop\fl.py", line 42, in <module>
    ft.app(target=main)
  File "C:\Users\jgogo\AppData\Local\Programs\Python\Python310\lib\site-packages\flet\flet.py", line 81, in app
    __app_sync(
  File "C:\Users\jgogo\AppData\Local\Programs\Python\Python310\lib\site-packages\flet\flet.py", line 160, in __app_sync
    conn.close()
  File "C:\Users\jgogo\AppData\Local\Programs\Python\Python310\lib\site-packages\flet\sync_local_socket_connection.py", line 129, in close
    if self.__uds_path and os.path.exists(self.__uds_path):
AttributeError: 'SyncLocalSocketConnection' object has no attribute '_SyncLocalSocketConnection__uds_path'. Did you mean: '_SyncLocalSocketConnection__recvall'?

Also, about half the time whenever I run the example code, the UI seems to indefinitely load. No error is shown: image The other half of the time it works as expected.

Edit: To be clear, I'm using the prerelease version of Flet, but using the synchronous example code provided in the documentation.

FeodorFitsner commented 1 year ago

Will fix that later today, looks like a bug!

FeodorFitsner commented 1 year ago

@JartanFTW it's been fixed in the latest pre-release package.

JartanFTW commented 1 year ago

The problem I previously mentioned was indeed fixed by the latest pre-release. Thank you!

Does an asynchronous alternative to ft.app(main) exist? I'm not sure what the exact terminology is so I'll use an example instead.

Old startup method for discord.py bots: bot.run('token') used in the same fashion as the flet process mentioned above.

Recently, discord.py introduced a new way to start the bots (the old way still works): await bot.start('token')

This allows for more freedom in advanced startup conditions, an example for discord.py provided here.

FeodorFitsner commented 1 year ago

@JartanFTW Yep, you can use await ft.app_async(main).

890r121289rh commented 1 year ago

@JartanFTW it's been fixed in the latest pre-release package.

@FeodorFitsner is there a way to install the pre-release package that fixes this issue on Windows, I'm having no luck and having the same issue?

JartanFTW commented 1 year ago

@JartanFTW it's been fixed in the latest pre-release package.

@FeodorFitsner is there a way to install the pre-release package that fixes this issue on Windows, I'm having no luck and having the same issue?

Simply re-running the pip install flet --pre command again does not actually update flet. You'll first need to run pip uninstall flet and then install it again.

890r121289rh commented 1 year ago

@JartanFTW it's been fixed in the latest pre-release package.

@FeodorFitsner is there a way to install the pre-release package that fixes this issue on Windows, I'm having no luck and having the same issue?

Simply re-running the pip install flet --pre command again does not actually update flet. You'll first need to run pip uninstall flet and then install it again.

@JartanFTW I've tried that already but still running into the same issue you were having.

JartanFTW commented 1 year ago

@JartanFTW I've tried that already but still running into the same issue you were having.

Can you provide the code you're running?

890r121289rh commented 1 year ago

@JartanFTW I've tried that already but still running into the same issue you were having.

Can you provide the code you're running?

Running any code with anything being async gives the error I've tried with all of the examples here:

So, async support has been merged into main and you can give it a try by installing pre-release version of Flet package with pip install flet --pre.

Async version of "counter" app:

import flet as ft

async def main(page: ft.Page):
    page.title = "Flet counter example"
    page.vertical_alignment = ft.MainAxisAlignment.CENTER

    txt_number = ft.TextField(value="0", text_align=ft.TextAlign.RIGHT, width=100)

    async def minus_click(e):
        txt_number.value = str(int(txt_number.value) - 1)
        await page.update_async()

    async def plus_click(e):
        txt_number.value = str(int(txt_number.value) + 1)
        await page.update_async()

    await page.add_async(
        ft.Row(
            [
                ft.IconButton(ft.icons.REMOVE, on_click=minus_click),
                txt_number,
                ft.IconButton(ft.icons.ADD, on_click=plus_click),
            ],
            alignment=ft.MainAxisAlignment.CENTER,
        )
    )

ft.app(main)

A few async samples:

* [Audio player](https://github.com/flet-dev/examples/pull/58/files#diff-f4afcaaa658d4528e4553a65ba045a27ee8046515272d50be2c7a5c485e06da9)

* [Simple OAuth](https://github.com/flet-dev/examples/pull/58/files#diff-c6a2af89cbfa2bacad9790976416d9ae63c1409651d79c2a01621ac7f1704c6f)

* [List GitHub repos](https://github.com/flet-dev/examples/pull/58/files#diff-e20c204d3f11a4ae5cb185b81c3d51f93c45753414a7309ce11c612ca111a644)
JartanFTW commented 1 year ago

Running any code with anything being async gives the error I've tried with all of the examples here:

Can you also please provide the error you are receiving? I know you've said it's the same as mine, but it can still be helpful to see.

890r121289rh commented 1 year ago

Actually it appears my issue is slightly different my mistake it shows:

Traceback (most recent call last):

File "D:\Users\User\Desktop\test.py", line 28, in ft.app(main) File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\flet\flet.py", line 67, in app asyncio.run( File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\asyncio\runners.py", line 190, in run
return runner.run(main) ^^^^^^^^^^^^^^^^ File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\asyncio\runners.py", line 118, in run return self._loop.run_until_complete(task) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\asyncio\base_events.py", line 653, in run_until_complete return future.result() ^^^^^^^^^^^^^^^ File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\flet\flet.py", line 238, in app_async await conn.close() File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\flet\async_local_socket_connection.py", line 158, in close if self.__uds_path and os.path.exists(self.uds_path): ^^^^^^^^^^^^^^^ AttributeError: 'AsyncLocalSocketConnection' object has no attribute '_AsyncLocalSocketConnectionuds_path'. Did you mean: '_AsyncLocalSocketConnection__port'?

JartanFTW commented 1 year ago

Actually it appears my issue is slightly different my mistake it shows:

This looks like a @FeodorFitsner thing. Good luck.

FeodorFitsner commented 1 year ago

@890r121289rh what do you have when running pip show flet?

890r121289rh commented 1 year ago

@890r121289rh what do you have when running pip show flet?

@FeodorFitsner

C:\Users\User>pip show flet Name: flet Version: 0.4.0.dev1070 Summary: Flet for Python - easily build interactive multi-platform apps in Python Home-page: Author: Appveyor Systems Inc. Author-email: hello@flet.dev License: MIT Location: C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages Requires: flet-core, httpx, oauthlib, packaging, watchdog, websocket-client, websockets Required-by:

FeodorFitsner commented 1 year ago

should be 0.4.0.dev1081.

Do pip install flet --upgrade --pre.

890r121289rh commented 1 year ago

should be 0.4.0.dev1081.

Do pip install flet --upgrade --pre.

I'm not able to get 0.4.0.dev1081 same for the previous version 0.4.0.dev1070 is the latest I can download

On pypi there is only a macos whl file: flet-0.4.0.dev1081-py3-none-macosx_10_14_x86_64.whl

FeodorFitsner commented 1 year ago

Ah, right! Sorry about that! I'm going to update it in a couple of minutes.

890r121289rh commented 1 year ago

Ah, right! Sorry about that! I'm going to update it in a couple of minutes.

Sounds good, and no worries I appreciate your help

FeodorFitsner commented 1 year ago

@890r121289rh OK, try doing pip install flet --upgrade --pre now. It should give you 0.4.0.dev1084. Let me know how that worked.

890r121289rh commented 1 year ago

@890r121289rh OK, try doing pip install flet --upgrade --pre now. It should give you 0.4.0.dev1084. Let me know how that worked.

It's working fine now thank you for your help

FeodorFitsner commented 1 year ago

The feature has been implemented and documented: https://flet.dev/docs/guides/python/async-apps