Explorar el Código

update ui.timer and lifecycle hooks

Falko Schindler hace 2 años
padre
commit
dcecec9e5b
Se han modificado 7 ficheros con 89 adiciones y 73 borrados
  1. 3 4
      api_docs_and_examples.py
  2. 17 1
      nicegui/globals.py
  3. 16 1
      nicegui/helpers.py
  4. 4 14
      nicegui/lifecycle.py
  5. 12 0
      nicegui/nicegui.py
  6. 36 53
      nicegui/timer.py
  7. 1 0
      nicegui/ui.py

+ 3 - 4
api_docs_and_examples.py

@@ -339,9 +339,8 @@ To overlay an SVG, make the `viewBox` exactly the size of the image and provide
             y2 = np.cos(x)
             line_plot.push([now], [[y1], [y2]])
 
-        # line_updates = ui.timer(0.1, update_line_plot, active=False)
-        # line_checkbox = ui.checkbox('active').bind_value(line_updates, 'active')
-        ui.button('update', on_click=update_line_plot)  # TODO: use ui.timer instead
+        line_updates = ui.timer(0.1, update_line_plot, active=False)
+        line_checkbox = ui.checkbox('active').bind_value(line_updates, 'active')
 
     @example(ui.linear_progress, skip=False)
     def linear_progress_example():
@@ -542,7 +541,7 @@ When NiceGUI is shut down or restarted, the startup tasks will be automatically
 
         # ui.on_connect(countdown)
 
-    # @example(ui.timer)
+    @example(ui.timer, skip=False)
     def timer_example():
         from datetime import datetime
 

+ 17 - 1
nicegui/globals.py

@@ -1,6 +1,7 @@
 import asyncio
 import logging
-from typing import TYPE_CHECKING, Callable, Dict, List, Optional
+from enum import Enum
+from typing import TYPE_CHECKING, Awaitable, Callable, Dict, List, Optional, Union
 
 from fastapi import FastAPI
 from socketio import AsyncServer
@@ -8,10 +9,19 @@ from socketio import AsyncServer
 if TYPE_CHECKING:
     from .client import Client
 
+
+class State(Enum):
+    STOPPED = 0
+    STARTING = 1
+    STARTED = 2
+    STOPPING = 3
+
+
 app: FastAPI
 sio: AsyncServer
 loop: Optional[asyncio.AbstractEventLoop] = None
 log: logging.Logger = logging.getLogger('nicegui')
+state: State = State.STOPPED
 
 host: str
 port: int
@@ -22,3 +32,9 @@ clients: Dict[int, 'Client'] = {}
 next_client_id: int = 0
 
 page_routes: Dict[Callable, str] = {}
+tasks: List[asyncio.tasks.Task] = []
+
+connect_handlers: List[Union[Callable, Awaitable]] = []
+disconnect_handlers: List[Union[Callable, Awaitable]] = []
+startup_handlers: List[Union[Callable, Awaitable]] = []
+shutdown_handlers: List[Union[Callable, Awaitable]] = []

+ 16 - 1
nicegui/helpers.py

@@ -2,7 +2,10 @@ import asyncio
 import functools
 import inspect
 import time
-from typing import Any
+from typing import Any, Awaitable, Callable, Union
+
+from . import globals
+from .task_logger import create_task
 
 
 def measure(*, reset: bool = False, ms: bool = False) -> None:
@@ -21,3 +24,15 @@ def is_coroutine(object: Any) -> bool:
     while isinstance(object, functools.partial):
         object = object.func
     return asyncio.iscoroutinefunction(object)
+
+
+def safe_invoke(func: Union[Callable, Awaitable]) -> None:
+    try:
+        if isinstance(func, Awaitable):
+            create_task(func)
+        else:
+            result = func()
+            if isinstance(result, Awaitable):
+                create_task(result)
+    except:
+        globals.log.exception(f'could not invoke {func}')

+ 4 - 14
nicegui/lifecycle.py

@@ -1,31 +1,21 @@
 from typing import Awaitable, Callable, Union
 
-import justpy as jp
-
 from . import globals
 
 
-def on_connect(self, handler: Union[Callable, Awaitable]):
+def on_connect(handler: Union[Callable, Awaitable]) -> None:
     globals.connect_handlers.append(handler)
 
 
-def on_disconnect(self, handler: Union[Callable, Awaitable]):
+def on_disconnect(handler: Union[Callable, Awaitable]) -> None:
     globals.disconnect_handlers.append(handler)
 
 
-def on_startup(self, handler: Union[Callable, Awaitable]):
+def on_startup(handler: Union[Callable, Awaitable]) -> None:
     if globals.state == globals.State.STARTED:
         raise RuntimeError('Unable to register another startup handler. NiceGUI has already been started.')
     globals.startup_handlers.append(handler)
 
 
-def on_shutdown(self, handler: Union[Callable, Awaitable]):
+def on_shutdown(handler: Union[Callable, Awaitable]) -> None:
     globals.shutdown_handlers.append(handler)
-
-
-async def shutdown(self) -> None:
-    if globals.config.reload:
-        raise Exception('ui.shutdown is not supported when auto-reload is enabled')
-    for socket in [s for page in jp.WebPage.sockets.values() for s in page.values()]:
-        await socket.close()
-    globals.server.should_exit = True

+ 12 - 0
nicegui/nicegui.py

@@ -11,6 +11,7 @@ from fastapi_socketio import SocketManager
 
 from . import binding, globals, vue
 from .client import Client
+from .helpers import safe_invoke
 from .task_logger import create_task
 
 globals.app = app = FastAPI()
@@ -39,8 +40,19 @@ def vue_dependencies(name: str):
 
 @app.on_event('startup')
 def on_startup() -> None:
+    globals.state = globals.State.STARTING
     globals.loop = asyncio.get_running_loop()
+    [safe_invoke(t) for t in globals.startup_handlers]
     create_task(binding.loop())
+    globals.state = globals.State.STARTED
+
+
+@app.on_event('shutdown')
+def shutdown() -> None:
+    globals.state = globals.State.STOPPING
+    [safe_invoke(t) for t in globals.shutdown_handlers]
+    [t.cancel() for t in globals.tasks]
+    globals.state = globals.State.STOPPED
 
 
 @sio.on('connect')

+ 36 - 53
nicegui/timer.py

@@ -1,31 +1,24 @@
 import asyncio
 import time
 import traceback
-from collections import namedtuple
-from typing import Callable, List, Optional
-
-from starlette.websockets import WebSocket
+from typing import Callable
 
 from . import globals
-from .auto_context import Context
 from .binding import BindableProperty
 from .helpers import is_coroutine
-from .page import Page, find_parent_page, find_parent_view
+from .lifecycle import on_startup
 from .task_logger import create_task
 
-NamedCoroutine = namedtuple('NamedCoroutine', ['name', 'coro'])
-
 
 class Timer:
-    prepared_coroutines: List[NamedCoroutine] = []
-
     active = BindableProperty()
     interval = BindableProperty()
 
-    def __init__(self, interval: float, callback: Callable, *, active: bool = True, once: bool = False):
+    def __init__(self, interval: float, callback: Callable, *, active: bool = True, once: bool = False) -> None:
         """Timer
 
-        One major drive behind the creation of NiceGUI was the necessity to have a simple approach to update the interface in regular intervals, for example to show a graph with incomming measurements.
+        One major drive behind the creation of NiceGUI was the necessity to have a simple approach to update the interface in regular intervals,
+        for example to show a graph with incoming measurements.
         A timer will execute a callback repeatedly with a given interval.
 
         :param interval: the interval in which the timer is called (can be changed during runtime)
@@ -33,48 +26,38 @@ class Timer:
         :param active: whether the callback should be executed or not (can be changed during runtime)
         :param once: whether the callback is only executed once after a delay specified by `interval` (default: `False`)
         """
-
-        self.active = active
         self.interval = interval
-        self.socket: Optional[WebSocket] = None
-        self.parent_page = find_parent_page()
-        self.parent_view = find_parent_view()
-
-        async def do_callback():
-            try:
-                with Context(self.parent_view) as context:
-                    result = callback()
-                    if is_coroutine(callback):
-                        await context.watch_asyncs(result)
-            except Exception:
-                traceback.print_exc()
+        self.callback = callback
+        self.active = active
 
-        async def timeout():
-            await asyncio.sleep(self.interval)
-            await do_callback()
+        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)))
+        else:
+            on_startup(coroutine)
 
-        async def loop():
-            while True:
-                if not self.parent_page.shared:
-                    sockets = list(Page.sockets.get(self.parent_page.page_id, {}).values())
-                    if not self.socket and sockets:
-                        self.socket = sockets[0]
-                    elif self.socket and not sockets:
-                        return
-                try:
-                    start = time.time()
-                    if self.active:
-                        await do_callback()
-                    dt = time.time() - start
-                    await asyncio.sleep(self.interval - dt)
-                except asyncio.CancelledError:
-                    return
-                except:
-                    traceback.print_exc()
-                    await asyncio.sleep(self.interval)
+    async def _run_once(self) -> None:
+        await asyncio.sleep(self.interval)
+        await self._invoke_callback()
 
-        coroutine = timeout() if once else loop()
-        if not (globals.loop and globals.loop.is_running()):
-            self.prepared_coroutines.append(NamedCoroutine(str(callback), coroutine))
-        else:
-            globals.tasks.append(create_task(coroutine, name=str(callback)))
+    async def _run_in_loop(self) -> None:
+        while True:
+            try:
+                start = time.time()
+                if self.active:
+                    await self._invoke_callback()
+                dt = time.time() - start
+                await asyncio.sleep(self.interval - dt)
+            except asyncio.CancelledError:
+                return
+            except:
+                traceback.print_exc()
+                await asyncio.sleep(self.interval)
+
+    async def _invoke_callback(self) -> None:
+        try:
+            result = self.callback()
+            if is_coroutine(self.callback):
+                await result
+        except Exception:
+            traceback.print_exc()

+ 1 - 0
nicegui/ui.py

@@ -43,4 +43,5 @@ from .elements.upload import Upload as upload
 from .notify import notify
 from .page import page
 from .run import run
+from .timer import Timer as timer
 from .update import update