1
0
Эх сурвалжийг харах

Merge pull request #115 from zauberzeug/javascript

Merge await_javascript and run_javascript
Rodja Trappe 2 жил өмнө
parent
commit
5eb34f4dbc

+ 11 - 11
api_docs_and_examples.py

@@ -684,21 +684,21 @@ It also enables you to identify sessions over [longer time spans by configuring
     javascript = '''#### JavaScript
 
 With `ui.run_javascript()` you can run arbitrary JavaScript code on a page that is executed in the browser.
-The asynchronous function will return after sending the command.
-
-With `ui.await_javascript()` you can send a JavaScript command and wait for its response.
-The asynchronous function will only return after receiving the result.
+The asynchronous function will return after the command(s) are executed.
+The result of the execution is returned as a dictionary containing the response string per websocket.
+You can also set `respond=False` to send a command without waiting for a response.
 '''
     with example(javascript):
-        async def run_javascript():
-            await ui.run_javascript('alert("Hello!")')
+        async def alert():
+            await ui.run_javascript('alert("Hello!")', respond=False)
 
-        async def await_javascript():
-            response = await ui.await_javascript('Date()')
-            ui.notify(f'Browser time: {response}')
+        async def get_date():
+            response = await ui.run_javascript('Date()')
+            for socket, time in response.items():
+                ui.notify(f'Browser time on host {socket.client.host}: {time}')
 
-        ui.button('run JavaScript', on_click=run_javascript)
-        ui.button('await JavaScript', on_click=await_javascript)
+        ui.button('fire and forget', on_click=alert)
+        ui.button('receive result', on_click=get_date)
 
     h3('Routes')
 

+ 49 - 40
nicegui/page.py

@@ -12,6 +12,7 @@ import justpy as jp
 from addict import Dict as AdDict
 from pygments.formatters import HtmlFormatter
 from starlette.requests import Request
+from starlette.websockets import WebSocket
 
 from . import globals
 from .helpers import is_coroutine
@@ -58,15 +59,16 @@ class Page(jp.QuasarPage):
         self.view.add_page(self)
 
     async def _route_function(self, request: Request) -> Page:
-        for handler in globals.connect_handlers + ([self.connect_handler] if self.connect_handler else []):
-            arg_count = len(inspect.signature(handler).parameters)
-            is_coro = is_coroutine(handler)
-            if arg_count == 1:
-                await handler(request) if is_coro else handler(request)
-            elif arg_count == 0:
-                await handler() if is_coro else handler()
-            else:
-                raise ValueError(f'invalid number of arguments (0 or 1 allowed, got {arg_count})')
+        with globals.within_view(self.view):
+            for handler in globals.connect_handlers + ([self.connect_handler] if self.connect_handler else []):
+                arg_count = len(inspect.signature(handler).parameters)
+                is_coro = is_coroutine(handler)
+                if arg_count == 1:
+                    await handler(request) if is_coro else handler(request)
+                elif arg_count == 0:
+                    await handler() if is_coro else handler()
+                else:
+                    raise ValueError(f'invalid number of arguments (0 or 1 allowed, got {arg_count})')
         return self
 
     async def handle_page_ready(self, msg: AdDict) -> bool:
@@ -87,57 +89,58 @@ class Page(jp.QuasarPage):
                     raise ValueError(f'invalid number of arguments (0 or 1 allowed, got {arg_count})')
         return False
 
-    async def on_disconnect(self, websocket=None) -> None:
-        for handler in globals.disconnect_handlers + ([self.disconnect_handler] if self.disconnect_handler else[]):
-            arg_count = len(inspect.signature(handler).parameters)
-            is_coro = is_coroutine(handler)
-            if arg_count == 1:
-                await handler(websocket) if is_coro else handler(websocket)
-            elif arg_count == 0:
-                await handler() if is_coro else handler()
-            else:
-                raise ValueError(f'invalid number of arguments (0 or 1 allowed, got {arg_count})')
+    async def on_disconnect(self, websocket: Optional[WebSocket] = None) -> None:
+        with globals.within_view(self.view):
+            for handler in globals.disconnect_handlers + ([self.disconnect_handler] if self.disconnect_handler else[]):
+                arg_count = len(inspect.signature(handler).parameters)
+                is_coro = is_coroutine(handler)
+                if arg_count == 1:
+                    await handler(websocket) if is_coro else handler(websocket)
+                elif arg_count == 0:
+                    await handler() if is_coro else handler()
+                else:
+                    raise ValueError(f'invalid number of arguments (0 or 1 allowed, got {arg_count})')
         await super().on_disconnect(websocket)
 
-    async def await_javascript(self, code: str, *, check_interval: float = 0.01, timeout: float = 1.0) -> str:
+    async def run_javascript_on_socket(self, code: str, websocket: WebSocket, *,
+                                       respond: bool = True, timeout: float = 1.0, check_interval: float = 0.01) -> Optional[str]:
         start_time = time.time()
         request_id = str(uuid.uuid4())
-        await self.run_javascript(code, request_id=request_id)
+        await websocket.send_json({'type': 'run_javascript', 'data': code, 'request_id': request_id, 'send': respond})
+        if not respond:
+            return
         while request_id not in self.waiting_javascript_commands:
             if time.time() > start_time + timeout:
                 raise TimeoutError('JavaScript did not respond in time')
             await asyncio.sleep(check_interval)
         return self.waiting_javascript_commands.pop(request_id)
 
-    def handle_javascript_result(self, msg) -> bool:
+    async def run_javascript(self, code: str, *,
+                             respond: bool = True, timeout: float = 1.0, check_interval: float = 0.01) -> Dict[WebSocket, Optional[str]]:
+        if self.page_id not in jp.WebPage.sockets:
+            raise RuntimeError('Cannot run JavaScript, because page is not ready.')
+        sockets = list(jp.WebPage.sockets[self.page_id].values())
+        results = await asyncio.gather(
+            *[self.run_javascript_on_socket(code, socket, respond=respond, timeout=timeout, check_interval=check_interval)
+              for socket in sockets], return_exceptions=True)
+        return dict(zip(sockets, results))
+
+    def handle_javascript_result(self, msg: AdDict) -> bool:
         self.waiting_javascript_commands[msg.request_id] = msg.result
         return False
 
 
 def add_head_html(self, html: str) -> None:
-    for page in find_parent_view().pages.values():
-        page.head_html += html
+    find_parent_page().head_html += html
 
 
 def add_body_html(self, html: str) -> None:
-    for page in find_parent_view().pages.values():
-        page.body_html += html
-
+    find_parent_page().body_html += html
 
-async def run_javascript(self, code: str) -> None:
-    for page in find_parent_view().pages.values():
-        assert isinstance(page, Page)
-        if page.page_id not in jp.WebPage.sockets:
-            raise RuntimeError('page not ready; use the `on_page_ready` argument: https://nicegui.io/#page')
-        await page.run_javascript(code)
 
-
-async def await_javascript(self, code: str, *, check_interval: float = 0.01, timeout: float = 1.0) -> None:
-    for page in find_parent_view().pages.values():
-        assert isinstance(page, Page)
-        if page.page_id not in jp.WebPage.sockets:
-            raise RuntimeError('page not ready; use the `on_page_ready` argument: https://nicegui.io/#page')
-        return await page.await_javascript(code, check_interval=check_interval, timeout=timeout)
+async def run_javascript(self, code: str, *,
+                         respond: bool = True, timeout: float = 1.0, check_interval: float = 0.01) -> Dict[WebSocket, Optional[str]]:
+    return await find_parent_page().run_javascript(code, respond=respond, timeout=timeout, check_interval=check_interval)
 
 
 class page:
@@ -246,6 +249,12 @@ def find_parent_view() -> jp.HTMLBaseComponent:
     return view_stack[-1]
 
 
+def find_parent_page() -> Page:
+    pages = list(find_parent_view().pages.values())
+    assert len(pages) == 1
+    return pages[0]
+
+
 def error(status_code: int, message: Optional[str] = None) -> Page:
     title = globals.config.title if globals.config else f'Error {status_code}'
     favicon = globals.config.favicon if globals.config else None

+ 1 - 1
nicegui/ui.py

@@ -5,7 +5,7 @@ import os
 class Ui:
     from .run import run  # NOTE: before justpy
 
-    from .page import page, add_head_html, add_body_html, run_javascript, await_javascript
+    from .page import page, add_head_html, add_body_html, run_javascript
     from .update import update
 
     from .elements.button import Button as button

+ 8 - 0
tests/test_auto_context.py

@@ -43,3 +43,11 @@ def test_adding_elements_with_async_await(screen: Screen):
             return
         screen.wait(0.1)
     raise AssertionError(f'{screen.render_content()} should show cards with "A" and "B"')
+
+
+def test_adding_elements_during_onconnect(screen: Screen):
+    ui.label('Label 1')
+    ui.on_connect(lambda: ui.label('Label 2'))
+
+    screen.open('/')
+    screen.should_contain('Label 2')

+ 7 - 8
tests/test_javascript.py

@@ -55,14 +55,13 @@ def test_executing_javascript_on_async_page(screen: Screen):
 
 
 def test_retrieving_content_from_javascript(screen: Screen):
-    async def write_time() -> None:
-        response = await ui.await_javascript('Date.now()')
-        ui.label(f'Browser time: {response}')
+    async def compute() -> None:
+        response = await ui.run_javascript('1 + 41')
+        for _, answer in response.items():
+            ui.label(answer)
 
-    ui.button('write time', on_click=write_time)
+    ui.button('compute', on_click=compute)
 
     screen.open('/')
-    screen.click('write time')
-    label = screen.find('Browser time').text
-    js_time = datetime.fromtimestamp(int(label.split(': ')[1]) / 1000)
-    assert abs((datetime.now() - js_time).total_seconds()) < 1, f'{js_time} should be close to now'
+    screen.click('compute')
+    screen.should_contain('42')