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

Merge pull request #115 from zauberzeug/javascript

Merge await_javascript and run_javascript
Rodja Trappe пре 2 година
родитељ
комит
5eb34f4dbc
5 измењених фајлова са 76 додато и 60 уклоњено
  1. 11 11
      api_docs_and_examples.py
  2. 49 40
      nicegui/page.py
  3. 1 1
      nicegui/ui.py
  4. 8 0
      tests/test_auto_context.py
  5. 7 8
      tests/test_javascript.py

+ 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
     javascript = '''#### JavaScript
 
 
 With `ui.run_javascript()` you can run arbitrary JavaScript code on a page that is executed in the browser.
 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):
     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')
     h3('Routes')
 
 

+ 49 - 40
nicegui/page.py

@@ -12,6 +12,7 @@ import justpy as jp
 from addict import Dict as AdDict
 from addict import Dict as AdDict
 from pygments.formatters import HtmlFormatter
 from pygments.formatters import HtmlFormatter
 from starlette.requests import Request
 from starlette.requests import Request
+from starlette.websockets import WebSocket
 
 
 from . import globals
 from . import globals
 from .helpers import is_coroutine
 from .helpers import is_coroutine
@@ -58,15 +59,16 @@ class Page(jp.QuasarPage):
         self.view.add_page(self)
         self.view.add_page(self)
 
 
     async def _route_function(self, request: Request) -> Page:
     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
         return self
 
 
     async def handle_page_ready(self, msg: AdDict) -> bool:
     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})')
                     raise ValueError(f'invalid number of arguments (0 or 1 allowed, got {arg_count})')
         return False
         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)
         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()
         start_time = time.time()
         request_id = str(uuid.uuid4())
         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:
         while request_id not in self.waiting_javascript_commands:
             if time.time() > start_time + timeout:
             if time.time() > start_time + timeout:
                 raise TimeoutError('JavaScript did not respond in time')
                 raise TimeoutError('JavaScript did not respond in time')
             await asyncio.sleep(check_interval)
             await asyncio.sleep(check_interval)
         return self.waiting_javascript_commands.pop(request_id)
         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
         self.waiting_javascript_commands[msg.request_id] = msg.result
         return False
         return False
 
 
 
 
 def add_head_html(self, html: str) -> None:
 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:
 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:
 class page:
@@ -246,6 +249,12 @@ def find_parent_view() -> jp.HTMLBaseComponent:
     return view_stack[-1]
     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:
 def error(status_code: int, message: Optional[str] = None) -> Page:
     title = globals.config.title if globals.config else f'Error {status_code}'
     title = globals.config.title if globals.config else f'Error {status_code}'
     favicon = globals.config.favicon if globals.config else None
     favicon = globals.config.favicon if globals.config else None

+ 1 - 1
nicegui/ui.py

@@ -5,7 +5,7 @@ import os
 class Ui:
 class Ui:
     from .run import run  # NOTE: before justpy
     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 .update import update
 
 
     from .elements.button import Button as button
     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
             return
         screen.wait(0.1)
         screen.wait(0.1)
     raise AssertionError(f'{screen.render_content()} should show cards with "A" and "B"')
     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):
 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.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')