Kaynağa Gözat

Allow updating elements from different thread.
This is accomplished by keeping the correct asyncio loop as global variable.
Now tests do not need to reload a page anymore and are hence much closer to the real behavior.

Rodja Trappe 2 yıl önce
ebeveyn
işleme
863876bc8f

+ 1 - 1
nicegui/binding.py

@@ -40,7 +40,7 @@ async def update_views_async(views: Set[HTMLBaseComponent]):
 
 
 
 
 def update_views(views: Set[HTMLBaseComponent]):
 def update_views(views: Set[HTMLBaseComponent]):
-    if asyncio._get_running_loop() is None:
+    if globals.loop is None:
         return  # NOTE: no need to update view if event loop is not running, yet
         return  # NOTE: no need to update view if event loop is not running, yet
     create_task(update_views_async(views), name='update_views_async')
     create_task(update_views_async(views), name='update_views_async')
 
 

+ 3 - 5
nicegui/elements/element.py

@@ -5,6 +5,7 @@ from typing import Dict, Optional
 
 
 import justpy as jp
 import justpy as jp
 
 
+from .. import globals
 from ..binding import BindableProperty, bind_from, bind_to
 from ..binding import BindableProperty, bind_from, bind_to
 from ..page import Page, get_current_view
 from ..page import Page, get_current_view
 from ..task_logger import create_task
 from ..task_logger import create_task
@@ -117,8 +118,5 @@ class Element:
         return self
         return self
 
 
     def update(self) -> None:
     def update(self) -> None:
-        try:
-            asyncio.get_running_loop()
-        except RuntimeError:
-            return
-        create_task(self.view.update())
+        if globals.loop is not None:
+            create_task(self.view.update())

+ 2 - 1
nicegui/elements/table.py

@@ -3,6 +3,7 @@ from typing import Dict
 
 
 import justpy as jp
 import justpy as jp
 
 
+from .. import globals
 from ..task_logger import create_task
 from ..task_logger import create_task
 from .element import Element
 from .element import Element
 
 
@@ -22,6 +23,6 @@ class Table(Element):
         view.options = self.options = jp.Dict(**options)
         view.options = self.options = jp.Dict(**options)
         super().__init__(view)
         super().__init__(view)
 
 
-        if not jp.template_options['aggrid'] and asyncio.get_event_loop().is_running():
+        if not jp.template_options['aggrid'] and globals.loop and globals.loop.is_running():
             create_task(self.page.run_javascript('location.reload()'))
             create_task(self.page.run_javascript('location.reload()'))
         jp.template_options['aggrid'] = True
         jp.template_options['aggrid'] = True

+ 1 - 1
nicegui/events.py

@@ -232,7 +232,7 @@ def handle_event(handler: Optional[Callable], arguments: EventArguments) -> Opti
         no_arguments = not signature(handler).parameters
         no_arguments = not signature(handler).parameters
         result = handler() if no_arguments else handler(arguments)
         result = handler() if no_arguments else handler(arguments)
         if is_coroutine(handler):
         if is_coroutine(handler):
-            if asyncio.get_event_loop().is_running():
+            if globals.loop and globals.loop.is_running():
                 create_task(result, name=str(handler))
                 create_task(result, name=str(handler))
             else:
             else:
                 on_startup(None, result)
                 on_startup(None, result)

+ 1 - 0
nicegui/globals.py

@@ -16,6 +16,7 @@ if TYPE_CHECKING:
 app: 'Starlette'
 app: 'Starlette'
 config: Optional['Config'] = None
 config: Optional['Config'] = None
 server: Optional[Server] = None
 server: Optional[Server] = None
+loop: Optional[asyncio.AbstractEventLoop] = None
 page_builders: Dict[str, 'PageBuilder'] = {}
 page_builders: Dict[str, 'PageBuilder'] = {}
 view_stack: List['jp.HTMLBaseComponent'] = []
 view_stack: List['jp.HTMLBaseComponent'] = []
 tasks: List[asyncio.tasks.Task] = []
 tasks: List[asyncio.tasks.Task] = []

+ 2 - 0
nicegui/nicegui.py

@@ -1,4 +1,5 @@
 # isort:skip_file
 # isort:skip_file
+import asyncio
 from typing import Awaitable, Callable
 from typing import Awaitable, Callable
 
 
 if True:  # NOTE: prevent formatter from mixing up these lines
 if True:  # NOTE: prevent formatter from mixing up these lines
@@ -27,6 +28,7 @@ async def patched_justpy_startup():
 
 
 @jp.app.on_event('startup')
 @jp.app.on_event('startup')
 def startup():
 def startup():
+    globals.loop = asyncio.get_running_loop()
     init_auto_index_page()
     init_auto_index_page()
     create_page_routes()
     create_page_routes()
     globals.tasks.extend(create_task(t.coro, name=t.name) for t in Timer.prepared_coroutines)
     globals.tasks.extend(create_task(t.coro, name=t.name) for t in Timer.prepared_coroutines)

+ 1 - 1
nicegui/routes.py

@@ -84,7 +84,7 @@ def add_dependencies(py_filepath: str, dependencies: List[str] = []) -> None:
         jp.app.routes.insert(0, Route(f'/{filename}', lambda _: FileResponse(vue_filepath)))
         jp.app.routes.insert(0, Route(f'/{filename}', lambda _: FileResponse(vue_filepath)))
         jp.component_file_list += [filename]
         jp.component_file_list += [filename]
 
 
-    if asyncio.get_event_loop().is_running():
+    if globals.loop and globals.loop.is_running():
         # NOTE: if new dependencies are added after starting the server, we need to reload the page on connected clients
         # NOTE: if new dependencies are added after starting the server, we need to reload the page on connected clients
         async def reload(view: jp.HTMLBaseComponent) -> None:
         async def reload(view: jp.HTMLBaseComponent) -> None:
             for page in view.pages.values():
             for page in view.pages.values():

+ 3 - 1
nicegui/task_logger.py

@@ -6,6 +6,8 @@ import logging
 import sys
 import sys
 from typing import Any, Awaitable, Optional, Tuple, TypeVar
 from typing import Any, Awaitable, Optional, Tuple, TypeVar
 
 
+from . import globals
+
 T = TypeVar('T')
 T = TypeVar('T')
 
 
 
 
@@ -26,7 +28,7 @@ def create_task(
     message = 'Task raised an exception'
     message = 'Task raised an exception'
     message_args = ()
     message_args = ()
     if loop is None:
     if loop is None:
-        loop = asyncio.get_running_loop()
+        loop = globals.loop
     if sys.version_info[1] < 8:
     if sys.version_info[1] < 8:
         task = loop.create_task(coroutine)  # name parameter is only supported from 3.8 onward
         task = loop.create_task(coroutine)  # name parameter is only supported from 3.8 onward
     else:
     else:

+ 2 - 1
nicegui/update.py

@@ -1,12 +1,13 @@
 import asyncio
 import asyncio
 from typing import List
 from typing import List
 
 
+from . import globals
 from .elements.element import Element
 from .elements.element import Element
 from .task_logger import create_task
 from .task_logger import create_task
 
 
 
 
 def update(self, *elements: List[Element]) -> None:
 def update(self, *elements: List[Element]) -> None:
-    if not asyncio.get_event_loop().is_running():
+    if not (gobals.loop and globals.loop.is_running()):
         return
         return
     for element in elements:
     for element in elements:
         create_task(element.view.update())
         create_task(element.view.update())

+ 14 - 9
tests/test_element.py

@@ -5,27 +5,32 @@ from .user import User
 
 
 def test_classes(user: User):
 def test_classes(user: User):
     label = ui.label('label')
     label = ui.label('label')
+    user.open('/')
 
 
-    def classes() -> str:
-        user.open('/')
-        return user.find('label').get_attribute('class')
+    def assert_classes(classes: str) -> None:
+        for i in range(20):
+            if user.find('label').get_attribute('class') == classes:
+                return
+            user.sleep(0.01)
+        else:
+            raise AssertionError(f'Expected {classes}, got {user.find("label").get_attribute("class")}')
 
 
-    assert classes() == ''
+    assert_classes('')
 
 
     label.classes('one')
     label.classes('one')
-    assert classes() == 'one'
+    assert_classes('one')
 
 
     label.classes('one')
     label.classes('one')
-    assert classes() == 'one'
+    assert_classes('one')
 
 
     label.classes('two three')
     label.classes('two three')
-    assert classes() == 'one two three'
+    assert_classes('one two three')
 
 
     label.classes(remove='two')
     label.classes(remove='two')
-    assert classes() == 'one three'
+    assert_classes('one three')
 
 
     label.classes(replace='four')
     label.classes(replace='four')
-    assert classes() == 'four'
+    assert_classes('four')
 
 
 
 
 def test_style(user: User):
 def test_style(user: User):