Преглед изворни кода

#338 introduce outbox; update elements during initialization, not when leaving container

Falko Schindler пре 2 година
родитељ
комит
3c00a8619b

+ 0 - 30
nicegui/async_updater.py

@@ -1,30 +0,0 @@
-from typing import Any, Coroutine, Generator
-
-from . import globals
-
-
-class AsyncUpdater:
-
-    def __init__(self, coro: Coroutine) -> None:
-        self.coro = coro
-
-    def __await__(self) -> Generator[Any, None, Any]:
-        coro_iter = self.coro.__await__()
-        iter_send, iter_throw = coro_iter.send, coro_iter.throw
-        send, message = iter_send, None
-        while True:
-            try:
-                signal = send(message)
-                self.lazy_update()
-            except StopIteration as err:
-                return err.value
-            else:
-                send = iter_send
-            try:
-                message = yield signal
-            except BaseException as err:
-                send, message = iter_throw, err
-
-    def lazy_update(self) -> None:
-        for slot in globals.get_slot_stack():
-            slot.lazy_update()

+ 3 - 3
nicegui/client.py

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

+ 7 - 4
nicegui/element.py

@@ -4,9 +4,9 @@ import json
 import re
 import re
 from abc import ABC
 from abc import ABC
 from copy import deepcopy
 from copy import deepcopy
-from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union
+from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
 
 
-from . import background_tasks, binding, events, globals, updates
+from . import binding, events, globals, outbox
 from .elements.mixins.visibility import Visibility
 from .elements.mixins.visibility import Visibility
 from .event_listener import EventListener
 from .event_listener import EventListener
 from .slot import Slot
 from .slot import Slot
@@ -39,6 +39,9 @@ class Element(ABC, Visibility):
         if slot_stack:
         if slot_stack:
             self.parent_slot = slot_stack[-1]
             self.parent_slot = slot_stack[-1]
             self.parent_slot.children.append(self)
             self.parent_slot.children.append(self)
+            outbox.enqueue_update(self.parent_slot.parent)
+
+        outbox.enqueue_update(self)
 
 
     def add_slot(self, name: str) -> Slot:
     def add_slot(self, name: str) -> Slot:
         self.slots[name] = Slot(self, name)
         self.slots[name] = Slot(self, name)
@@ -180,13 +183,13 @@ class Element(ABC, Visibility):
         return ids
         return ids
 
 
     def update(self) -> None:
     def update(self) -> None:
-        updates.enqueue(self)
+        outbox.enqueue_update(self)
 
 
     def run_method(self, name: str, *args: Any) -> None:
     def run_method(self, name: str, *args: Any) -> None:
         if not globals.loop:
         if not globals.loop:
             return
             return
         data = {'id': self.id, 'name': name, 'args': args}
         data = {'id': self.id, 'name': name, 'args': args}
-        background_tasks.create(globals.sio.emit('run_method', data, room=globals._socket_id or self.client.id))
+        outbox.enqueue_message('run_method', data, globals._socket_id or self.client.id)
 
 
     def clear(self) -> None:
     def clear(self) -> None:
         descendants = [self.client.elements[id] for id in self.collect_descendant_ids()[1:]]
         descendants = [self.client.elements[id] for id in self.collect_descendant_ids()[1:]]

+ 1 - 1
nicegui/events.py

@@ -273,7 +273,7 @@ def handle_event(handler: Optional[Callable],
         if is_coroutine(handler):
         if is_coroutine(handler):
             async def wait_for_result():
             async def wait_for_result():
                 with sender.parent_slot:
                 with sender.parent_slot:
-                    await AsyncUpdater(result)
+                    await result
             if globals.loop and globals.loop.is_running():
             if globals.loop and globals.loop.is_running():
                 background_tasks.create(wait_for_result(), name=str(handler))
                 background_tasks.create(wait_for_result(), name=str(handler))
             else:
             else:

+ 2 - 2
nicegui/functions/notify.py

@@ -1,6 +1,6 @@
 from typing import Optional, Union
 from typing import Optional, Union
 
 
-from .. import background_tasks, globals
+from .. import globals, outbox
 
 
 
 
 def notify(message: str, *,
 def notify(message: str, *,
@@ -23,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>`_.
     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}
     options = {key: value for key, value in locals().items() if not key.startswith('_') and value is not None}
-    background_tasks.create(globals.sio.emit('notify', options, room=globals.get_client().id))
+    outbox.enqueue_message('notify', options, globals.get_client().id)

+ 1 - 1
nicegui/functions/timer.py

@@ -75,7 +75,7 @@ class Timer:
         try:
         try:
             result = self.callback()
             result = self.callback()
             if is_coroutine(self.callback):
             if is_coroutine(self.callback):
-                await AsyncUpdater(result)
+                await result
         except Exception:
         except Exception:
             traceback.print_exc()
             traceback.print_exc()
 
 

+ 2 - 2
nicegui/nicegui.py

@@ -10,7 +10,7 @@ from fastapi.responses import FileResponse, Response
 from fastapi.staticfiles import StaticFiles
 from fastapi.staticfiles import StaticFiles
 from fastapi_socketio import SocketManager
 from fastapi_socketio import SocketManager
 
 
-from . import background_tasks, binding, globals, updates
+from . import background_tasks, binding, globals, outbox
 from .app import App
 from .app import App
 from .client import Client
 from .client import Client
 from .dependencies import js_components, js_dependencies
 from .dependencies import js_components, js_dependencies
@@ -61,7 +61,7 @@ def handle_startup(with_welcome_message: bool = True) -> None:
         for t in globals.startup_handlers:
         for t in globals.startup_handlers:
             safe_invoke(t)
             safe_invoke(t)
     background_tasks.create(binding.loop())
     background_tasks.create(binding.loop())
-    background_tasks.create(updates.loop())
+    background_tasks.create(outbox.loop())
     background_tasks.create(prune_clients())
     background_tasks.create(prune_clients())
     background_tasks.create(prune_slot_stacks())
     background_tasks.create(prune_slot_stacks())
     globals.state = globals.State.STARTED
     globals.state = globals.State.STARTED

+ 38 - 0
nicegui/outbox.py

@@ -0,0 +1,38 @@
+import asyncio
+from collections import deque
+from typing import TYPE_CHECKING, Any, Deque, Literal, Tuple
+
+from . import globals
+
+if TYPE_CHECKING:
+    from .element import Element
+    ClientId = int
+    MessageType = Literal['update', 'run_method', 'run_javascript', 'open', 'notify']
+    MessageGroup = Tuple[ClientId, MessageType, Any]
+
+queue: Deque['MessageGroup'] = deque()
+
+
+def enqueue_update(element: 'Element') -> None:
+    if queue:
+        client_id, message_type, argument = queue[-1]
+        if client_id == element.client.id and message_type == 'update':
+            elements: Deque[Element] = argument
+            elements.append(element)
+            return
+    queue.append((element.client.id, 'update', deque([element])))
+
+
+def enqueue_message(message_type: 'MessageType', data: Any, client_id: 'ClientId') -> None:
+    queue.append((client_id, message_type, data))
+
+
+async def loop() -> None:
+    while True:
+        while queue:
+            client_id, message_type, data = queue.popleft()
+            if message_type == 'update':
+                messages: Deque[Element] = data
+                data = {'elements': {e.id: e.to_dict() for e in messages}}
+            await globals.sio.emit(message_type, data, room=client_id)
+        await asyncio.sleep(0.01)

+ 1 - 1
nicegui/page.py

@@ -65,7 +65,7 @@ class page:
             if inspect.isawaitable(result):
             if inspect.isawaitable(result):
                 async def wait_for_result() -> None:
                 async def wait_for_result() -> None:
                     with client:
                     with client:
-                        await AsyncUpdater(result)
+                        await result
                 task = background_tasks.create(wait_for_result())
                 task = background_tasks.create(wait_for_result())
                 deadline = time.time() + self.response_timeout
                 deadline = time.time() + self.response_timeout
                 while task and not client.is_waiting_for_connection and not task.done():
                 while task and not client.is_waiting_for_connection and not task.done():

+ 0 - 8
nicegui/slot.py

@@ -12,19 +12,11 @@ class Slot:
         self.name = name
         self.name = name
         self.parent = parent
         self.parent = parent
         self.children: List['Element'] = []
         self.children: List['Element'] = []
-        self.child_count = 0
 
 
     def __enter__(self):
     def __enter__(self):
-        self.child_count = len(self.children)
         globals.get_slot_stack().append(self)
         globals.get_slot_stack().append(self)
         return self
         return self
 
 
     def __exit__(self, *_):
     def __exit__(self, *_):
         globals.get_slot_stack().pop()
         globals.get_slot_stack().pop()
         globals.prune_slot_stack()
         globals.prune_slot_stack()
-        self.lazy_update()
-
-    def lazy_update(self) -> None:
-        if self.child_count != len(self.children):
-            self.child_count = len(self.children)
-            self.parent.update()

+ 0 - 41
nicegui/updates.py

@@ -1,41 +0,0 @@
-import asyncio
-from typing import TYPE_CHECKING, Dict, List, Set
-
-from . import globals
-
-if TYPE_CHECKING:
-    from .element import Element
-
-update_queue: Dict[int, List] = {}  # element id -> [element, attributes]
-
-
-def enqueue(element: 'Element', *attributes: str) -> None:
-    '''Schedules a UI update for this element.
-
-    Attributes can be 'class', 'style', 'props', or 'text'.
-    '''
-    if element.id not in update_queue:
-        update_queue[element.id] = [element, list(attributes)]
-    else:
-        queued_attributes: Set[str] = update_queue[element.id][1]
-        if queued_attributes and attributes:
-            queued_attributes.update(attributes)
-        else:
-            queued_attributes.clear()
-
-
-async def loop() -> None:
-    '''Repeatedly updates all elements in the update queue.'''
-    while True:
-        elements: Dict[int, 'Element'] = {}
-        for id, value in sorted(update_queue.items()):  # NOTE: sort by element ID to process parents before children
-            if id in elements:
-                continue
-            element: 'Element' = value[0]
-            for id in element.collect_descendant_ids():
-                elements[id] = element.client.elements[id].to_dict()
-        if elements:
-            await globals.sio.emit('update', {'elements': elements}, room=element.client.id)
-            update_queue.clear()
-        else:
-            await asyncio.sleep(0.01)

+ 1 - 0
tests/test_dialog.py

@@ -16,6 +16,7 @@ def test_open_close_dialog(screen: Screen):
     screen.open('/')
     screen.open('/')
     screen.should_not_contain('Content')
     screen.should_not_contain('Content')
     screen.click('Open')
     screen.click('Open')
+    screen.wait(0.5)
     screen.should_contain('Content')
     screen.should_contain('Content')
     screen.click('Close')
     screen.click('Close')
     screen.wait(0.5)
     screen.wait(0.5)

+ 3 - 0
tests/test_element.py

@@ -120,16 +120,19 @@ def test_remove_and_clear(screen: Screen):
     screen.should_contain('Label C')
     screen.should_contain('Label C')
 
 
     screen.click('Remove B')
     screen.click('Remove B')
+    screen.wait(0.5)
     screen.should_contain('Label A')
     screen.should_contain('Label A')
     screen.should_not_contain('Label B')
     screen.should_not_contain('Label B')
     screen.should_contain('Label C')
     screen.should_contain('Label C')
 
 
     screen.click('Remove 0')
     screen.click('Remove 0')
+    screen.wait(0.5)
     screen.should_not_contain('Label A')
     screen.should_not_contain('Label A')
     screen.should_not_contain('Label B')
     screen.should_not_contain('Label B')
     screen.should_contain('Label C')
     screen.should_contain('Label C')
 
 
     screen.click('Clear')
     screen.click('Clear')
+    screen.wait(0.5)
     screen.should_not_contain('Label A')
     screen.should_not_contain('Label A')
     screen.should_not_contain('Label B')
     screen.should_not_contain('Label B')
     screen.should_not_contain('Label C')
     screen.should_not_contain('Label C')

+ 1 - 0
tests/test_expansion.py

@@ -13,6 +13,7 @@ def test_open_close_expansion(screen: Screen):
     screen.should_contain('Expansion')
     screen.should_contain('Expansion')
     screen.should_not_contain('Content')
     screen.should_not_contain('Content')
     screen.click('Open')
     screen.click('Open')
+    screen.wait(0.5)
     screen.should_contain('Content')
     screen.should_contain('Content')
     screen.click('Close')
     screen.click('Close')
     screen.wait(0.5)
     screen.wait(0.5)

+ 3 - 0
tests/test_input.py

@@ -29,6 +29,7 @@ def test_password(screen: Screen):
     assert element.get_attribute('value') == '123456'
     assert element.get_attribute('value') == '123456'
 
 
     element.send_keys('789')
     element.send_keys('789')
+    screen.wait(0.5)
     assert element.get_attribute('value') == '123456789'
     assert element.get_attribute('value') == '123456789'
 
 
 
 
@@ -44,7 +45,9 @@ def test_toggle_button(screen: Screen):
     assert element.get_attribute('value') == '123456'
     assert element.get_attribute('value') == '123456'
 
 
     screen.click('visibility_off')
     screen.click('visibility_off')
+    screen.wait(0.5)
     assert element.get_attribute('type') == 'text'
     assert element.get_attribute('type') == 'text'
 
 
     screen.click('visibility')
     screen.click('visibility')
+    screen.wait(0.5)
     assert element.get_attribute('type') == 'password'
     assert element.get_attribute('type') == 'password'