zauberzeug / nicegui

Create web-based user interfaces with Python. The nice way.
https://nicegui.io
MIT License
10.12k stars 605 forks source link

asyncio not using ProactorEventLoop with reload enabled #3874

Open lausfl opened 1 month ago

lausfl commented 1 month ago

Description

Version information:

I was unable to find any documentation or existing issues about this and I am not sure if I am doing something wrong here.

I want to create an application that uses NiceGUI and has a separate process running from which I can parse the output on stdout and do something with it. The problem I encounter is that I am unable to start the process when the reload argument of ui.run() is set to True (a NotImplementedError exception is thrown by asyncio). On further investigation I found that the asyncio event loop is set to _WindowsSelectorEventLoop when reload is enabled and to ProactorEventLoop when it's disabled.

I found the following issue, which seems to be related: https://github.com/encode/uvicorn/issues/1220

So I tried setting the event loop policy to try and force it to use a ProactorEventLoop, but to no avail.

Here's the MRE:

import asyncio

asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
from nicegui import ui, app

def on_packet(packet):
    print('packet received')

async def start_ping():
    print(type(asyncio.get_event_loop()).__name__)
    cmd = ['ping', '127.0.0.1']
    proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE)
    async for line in proc.stdout:
        print(line)

ui.label('Hello NiceGUI!')

app.on_startup(start_ping)
ui.run(reload=True)

When I change the above code to ui.run(reload=False), it works as intended.

rodja commented 1 month ago

Have you tried using a main guard? The subprocess evaluates the main file again, but should not start another NiceGUI application.

lausfl commented 1 month ago

@rodja do you mean like this?

import asyncio

asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
from nicegui import ui, app

def on_packet(packet):
    print('packet received')

async def start_ping():
    print(type(asyncio.get_event_loop()).__name__)
    cmd = ['ping', '127.0.0.1']
    proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE)
    async for line in proc.stdout:
        print(line)

if __name__ == '__mp_main__':
    ui.label('Hello NiceGUI!')

app.on_startup(start_ping)
ui.run(reload=True)

I also tried putting app.on_startup(start_ping) inside the guard. Either way I get the same behaviour.

rodja commented 1 month ago

The ui.run() must be guarded:

...
if __name__ in ('__mp_main__', '__main__'):
    ui.run()
lausfl commented 1 month ago

It's still using the _WindowsSelectorEventLoop with the guard. I also tried putting all three lines inside the guard, but nothing changes. Is this only happening on my end or can you reproduce it?

import asyncio

asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
from nicegui import ui, app

def on_packet(packet):
    print('packet received')

async def start_ping():
    print(type(asyncio.get_event_loop()).__name__)
    cmd = ['ping', '127.0.0.1']
    proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE)
    async for line in proc.stdout:
        print(line)

ui.label('Hello NiceGUI!')
app.on_startup(start_ping)

if __name__ in ('__mp_main__', '__main__'):
    ui.run()
rodja commented 1 month ago

We do not use Windows. Maybe someone from the community?