Bläddra i källkod

Merge branch 'main' into client_connections

# Conflicts:
#	nicegui/functions/timer.py
#	nicegui/nicegui.py
#	tests/test_page.py
Falko Schindler 2 år sedan
förälder
incheckning
3c7b3bc695

+ 7 - 1
nicegui/task_logger.py → nicegui/background_tasks.py

@@ -10,8 +10,10 @@ from . import globals
 
 T = TypeVar('T')
 
+running_tasks = set()
 
-def create_task(
+
+def create(
     coroutine: Awaitable[T],
     *,
     loop: Optional[asyncio.AbstractEventLoop] = None,
@@ -22,6 +24,8 @@ def create_task(
     an exception handler added to the resulting task. If the task raises an exception it is logged
     using the provided ``logger``, with additional context provided by ``message`` and optionally
     ``message_args``.
+    Also a reference to the task is kept until it is done, so that the task is not garbage collected mid-execution.
+    See https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task.
     '''
     logger = logging.getLogger(__name__)
     message = 'Task raised an exception'
@@ -36,6 +40,8 @@ def create_task(
     task.add_done_callback(
         functools.partial(_handle_task_result, logger=logger, message=message, message_args=message_args)
     )
+    running_tasks.add(task)
+    task.add_done_callback(running_tasks.discard)
     return task
 
 

+ 3 - 4
nicegui/client.py

@@ -9,11 +9,10 @@ from fastapi import Request
 from fastapi.responses import Response
 from fastapi.templating import Jinja2Templates
 
-from . import globals
+from . import background_tasks, globals
 from .dependencies import generate_js_imports, generate_vue_content
 from .element import Element
 from .favicon import get_favicon_url
-from .task_logger import create_task
 
 if TYPE_CHECKING:
     from .page import page
@@ -114,7 +113,7 @@ class Client:
             'code': code,
             'request_id': request_id if respond else None,
         }
-        create_task(globals.sio.emit('run_javascript', command, room=self.id))
+        background_tasks.create(globals.sio.emit('run_javascript', command, room=self.id))
         if not respond:
             return None
         deadline = time.time() + timeout
@@ -126,7 +125,7 @@ class Client:
 
     def open(self, target: Union[Callable, str]) -> None:
         path = target if isinstance(target, str) else globals.page_routes[target]
-        create_task(globals.sio.emit('open', path, room=self.id))
+        background_tasks.create(globals.sio.emit('open', path, room=self.id))
 
     def on_connect(self, handler: Union[Callable, Awaitable]) -> None:
         self.connect_handlers.append(handler)

+ 4 - 5
nicegui/element.py

@@ -5,11 +5,10 @@ from abc import ABC
 from copy import deepcopy
 from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union
 
-from . import binding, globals
+from . import background_tasks, binding, globals
 from .elements.mixins.visibility import Visibility
 from .event_listener import EventListener
 from .slot import Slot
-from .task_logger import create_task
 
 if TYPE_CHECKING:
     from .client import Client
@@ -153,7 +152,7 @@ class Element(ABC, Visibility):
             if listener.type == msg['type']:
                 result = listener.handler(msg)
                 if isinstance(result, Awaitable):
-                    create_task(result)
+                    background_tasks.create(result)
 
     def collect_descendant_ids(self) -> List[int]:
         '''includes own ID as first element'''
@@ -168,13 +167,13 @@ class Element(ABC, Visibility):
             return
         ids = self.collect_descendant_ids()
         elements = {id: self.client.elements[id].to_dict() for id in ids}
-        create_task(globals.sio.emit('update', {'elements': elements}, room=self.client.id))
+        background_tasks.create(globals.sio.emit('update', {'elements': elements}, room=self.client.id))
 
     def run_method(self, name: str, *args: Any) -> None:
         if not globals.loop:
             return
         data = {'id': self.id, 'name': name, 'args': args}
-        create_task(globals.sio.emit('run_method', data, room=globals._socket_id or self.client.id))
+        background_tasks.create(globals.sio.emit('run_method', data, room=globals._socket_id or self.client.id))
 
     def clear(self) -> None:
         descendants = [self.client.elements[id] for id in self.collect_descendant_ids()[1:]]

+ 2 - 3
nicegui/elements/plot.py

@@ -3,9 +3,8 @@ import io
 
 import matplotlib.pyplot as plt
 
-from .. import globals
+from .. import background_tasks, globals
 from ..element import Element
-from ..task_logger import create_task
 
 
 class Plot(Element):
@@ -24,7 +23,7 @@ class Plot(Element):
         self._convert_to_html()
 
         if not self.client.shared:
-            create_task(self._auto_close(), name='auto-close plot figure')
+            background_tasks.create(self._auto_close(), name='auto-close plot figure')
 
     def _convert_to_html(self) -> None:
         with io.StringIO() as output:

+ 2 - 3
nicegui/events.py

@@ -3,11 +3,10 @@ from dataclasses import dataclass
 from inspect import signature
 from typing import TYPE_CHECKING, Any, BinaryIO, Callable, List, Optional
 
-from . import globals
+from . import background_tasks, globals
 from .async_updater import AsyncUpdater
 from .client import Client
 from .helpers import is_coroutine
-from .task_logger import create_task
 
 if TYPE_CHECKING:
     from .element import Element
@@ -273,7 +272,7 @@ def handle_event(handler: Optional[Callable], arguments: EventArguments) -> None
                 with arguments.sender.parent_slot:
                     await AsyncUpdater(result)
             if globals.loop and globals.loop.is_running():
-                create_task(wait_for_result(), name=str(handler))
+                background_tasks.create(wait_for_result(), name=str(handler))
             else:
                 globals.app.on_startup(wait_for_result())
     except Exception:

+ 2 - 3
nicegui/functions/notify.py

@@ -1,7 +1,6 @@
 from typing import Optional, Union
 
-from .. import globals
-from ..task_logger import create_task
+from .. import background_tasks, globals
 
 
 def notify(message: str, *,
@@ -24,4 +23,4 @@ def notify(message: str, *,
     Note: You can pass additional keyword arguments according to `Quasar's Notify API <https://quasar.dev/quasar-plugins/notify#notify-api>`_.
     """
     options = {key: value for key, value in locals().items() if not key.startswith('_') and value is not None}
-    create_task(globals.sio.emit('notify', options, room=globals.get_client().id))
+    background_tasks.create(globals.sio.emit('notify', options, room=globals.get_client().id))

+ 2 - 3
nicegui/functions/timer.py

@@ -3,11 +3,10 @@ import time
 import traceback
 from typing import Callable
 
-from .. import globals
+from .. import background_tasks, globals
 from ..async_updater import AsyncUpdater
 from ..binding import BindableProperty
 from ..helpers import is_coroutine
-from ..task_logger import create_task
 
 
 class Timer:
@@ -33,7 +32,7 @@ class Timer:
 
         coroutine = self._run_once if once else self._run_in_loop
         if globals.state == globals.State.STARTED:
-            globals.tasks.append(create_task(coroutine(), name=str(callback)))
+            background_tasks.create(coroutine(), name=str(callback))
         else:
             globals.app.on_startup(coroutine)
 

+ 0 - 1
nicegui/globals.py

@@ -45,7 +45,6 @@ clients: Dict[str, 'Client'] = {}
 index_client: 'Client'
 
 page_routes: Dict[Callable, str] = {}
-tasks: List[asyncio.tasks.Task] = []
 
 startup_handlers: List[Union[Callable, Awaitable]] = []
 shutdown_handlers: List[Union[Callable, Awaitable]] = []

+ 3 - 4
nicegui/helpers.py

@@ -4,9 +4,8 @@ import inspect
 from contextlib import nullcontext
 from typing import Any, Awaitable, Callable, Optional, Union
 
-from . import globals
+from . import background_tasks, globals
 from .client import Client
-from .task_logger import create_task
 
 
 def is_coroutine(object: Any) -> bool:
@@ -21,7 +20,7 @@ def safe_invoke(func: Union[Callable, Awaitable], client: Optional[Client] = Non
             async def func_with_client():
                 with client or nullcontext():
                     await func
-            create_task(func_with_client())
+            background_tasks.create(func_with_client())
         else:
             with client or nullcontext():
                 result = func(client) if len(inspect.signature(func).parameters) == 1 and client is not None else func()
@@ -29,6 +28,6 @@ def safe_invoke(func: Union[Callable, Awaitable], client: Optional[Client] = Non
                 async def result_with_client():
                     with client or nullcontext():
                         await result
-                create_task(result_with_client())
+                background_tasks.create(result_with_client())
     except:
         globals.log.exception(f'could not invoke {func}')

+ 5 - 6
nicegui/nicegui.py

@@ -10,7 +10,7 @@ from fastapi.responses import FileResponse, Response
 from fastapi.staticfiles import StaticFiles
 from fastapi_socketio import SocketManager
 
-from . import binding, globals
+from . import background_tasks, binding, globals
 from .app import App
 from .client import Client
 from .dependencies import js_components, js_dependencies
@@ -18,7 +18,6 @@ from .element import Element
 from .error import error_content
 from .helpers import safe_invoke
 from .page import page
-from .task_logger import create_task
 
 globals.app = app = App()
 globals.sio = sio = SocketManager(app=app)._sio
@@ -52,9 +51,9 @@ def handle_startup(with_welcome_message: bool = True) -> None:
     globals.loop = asyncio.get_running_loop()
     for t in globals.startup_handlers:
         safe_invoke(t)
-    create_task(binding.loop())
-    create_task(prune_clients())
-    create_task(prune_slot_stacks())
+    background_tasks.create(binding.loop())
+    background_tasks.create(prune_clients())
+    background_tasks.create(prune_slot_stacks())
     globals.state = globals.State.STARTED
     if with_welcome_message:
         print(f'NiceGUI ready to go on http://{globals.host}:{globals.port}')
@@ -65,7 +64,7 @@ def handle_shutdown() -> None:
     globals.state = globals.State.STOPPING
     for t in globals.shutdown_handlers:
         safe_invoke(t)
-    for t in globals.tasks:
+    for t in background_tasks.running_tasks:
         t.cancel()
     globals.state = globals.State.STOPPED
 

+ 2 - 3
nicegui/page.py

@@ -5,11 +5,10 @@ from typing import Callable, Optional
 
 from fastapi import Request, Response
 
-from . import globals
+from . import background_tasks, globals
 from .async_updater import AsyncUpdater
 from .client import Client
 from .favicon import create_favicon_route
-from .task_logger import create_task
 
 
 class page:
@@ -62,7 +61,7 @@ class page:
                 async def wait_for_result() -> None:
                     with client:
                         await AsyncUpdater(result)
-                task = create_task(wait_for_result())
+                task = background_tasks.create(wait_for_result())
                 deadline = time.time() + self.response_timeout
                 while task and not client.is_waiting_for_connection and not task.done():
                     if time.time() > deadline:

+ 3 - 4
tests/test_auto_context.py

@@ -2,8 +2,7 @@ import asyncio
 
 from selenium.webdriver.common.by import By
 
-from nicegui import Client, ui
-from nicegui.task_logger import create_task
+from nicegui import Client, background_tasks, ui
 
 from .screen import Screen
 
@@ -120,8 +119,8 @@ def test_adding_elements_from_different_tasks(screen: Screen):
             await asyncio.sleep(1.0)
 
     screen.open('/')
-    create_task(add_label1())
-    create_task(add_label2())
+    background_tasks.create(add_label1())
+    background_tasks.create(add_label2())
     screen.wait_for('1')
     screen.wait_for('2')
     c1 = screen.selenium.find_element(By.ID, card1.id)

+ 2 - 2
tests/test_page.py

@@ -1,7 +1,7 @@
 import asyncio
 from uuid import uuid4
 
-from nicegui import Client, task_logger, ui
+from nicegui import Client, background_tasks, ui
 
 from .screen import Screen
 
@@ -102,7 +102,7 @@ def test_wait_for_connected(screen: Screen):
     async def load() -> None:
         label.text = 'loading...'
         # NOTE we can not use asyncio.create_task() here because we are on a different thread than the NiceGUI event loop
-        task_logger.create_task(takes_a_while())
+        background_tasks.create(takes_a_while())
 
     async def takes_a_while() -> None:
         await asyncio.sleep(0.1)