瀏覽代碼

Merge pull request #1880 from zauberzeug/awaitable-response

Improve robustness and documentation for AwaitableResponse
Rodja Trappe 1 年之前
父節點
當前提交
1d361e5ac1

+ 0 - 2
nicegui/__init__.py

@@ -1,7 +1,6 @@
 from . import context, elements, run, ui
 from .api_router import APIRouter
 from .app.app import App
-from .awaitable_response import AwaitableResponse
 from .client import Client
 from .nicegui import app
 from .tailwind import Tailwind
@@ -11,7 +10,6 @@ __all__ = [
     'APIRouter',
     'app',
     'App',
-    'AwaitableResponse',
     'Client',
     'context',
     'elements',

+ 28 - 11
nicegui/awaitable_response.py

@@ -1,31 +1,48 @@
 from __future__ import annotations
 
-from asyncio import Task
-from typing import Callable, Optional
+from typing import Callable
 
 from . import background_tasks
 
 
 class AwaitableResponse:
 
-    def __init__(self, fire_and_forget: Optional[Callable], wait_for_result: Optional[Callable]) -> None:
+    def __init__(self, fire_and_forget: Callable, wait_for_result: Callable) -> None:
         """Awaitable Response
 
         This class can be used to run one of two different callables, depending on whether the response is awaited or not.
+        It must be awaited immediately after creation or not at all.
 
         :param fire_and_forget: The callable to run if the response is not awaited.
         :param wait_for_result: The callable to run if the response is awaited.
         """
-        self.fire_and_forget_task: Optional[Task] = \
-            background_tasks.create(self._start(fire_and_forget), name='fire and forget') if fire_and_forget else None
+        self.fire_and_forget = fire_and_forget
         self.wait_for_result = wait_for_result
+        self._is_fired = False
+        self._is_awaited = False
+        background_tasks.create(self._fire(), name='fire')
 
-    async def _start(self, command: Callable) -> None:
-        command()
+    async def _fire(self) -> None:
+        if self._is_awaited:
+            return
+        self._is_fired = True
+        self.fire_and_forget()
 
     def __await__(self):
-        if self.fire_and_forget_task is not None:
-            self.fire_and_forget_task.cancel()
-        if self.wait_for_result is None:
-            raise ValueError('AwaitableResponse has no result to await')
+        if self._is_fired:
+            raise RuntimeError('AwaitableResponse must be awaited immediately after creation or not at all')
+        self._is_awaited = True
         return self.wait_for_result().__await__()
+
+
+class NullResponse(AwaitableResponse):
+
+    def __init__(self) -> None:  # pylint: disable=super-init-not-called
+        """Null Response
+
+        This class can be used to create an AwaitableResponse that does nothing.
+        In contrast to AwaitableResponse, it can be created without a running event loop.
+        """
+
+    def __await__(self):
+        yield from []

+ 9 - 0
nicegui/client.py

@@ -151,6 +151,15 @@ class Client:
 
         The client connection must be established before this method is called.
         You can do this by `await client.connected()` or register a callback with `client.on_connect(...)`.
+
+        If the function is awaited, the result of the JavaScript code is returned.
+        Otherwise, the JavaScript code is executed without waiting for a response.
+
+        :param code: JavaScript code to run
+        :param timeout: timeout in seconds (default: `1.0`)
+        :param check_interval: interval in seconds to check for a response (default: `0.01`)
+
+        :return: AwaitableResponse that can be awaited to get the result of the JavaScript code
         """
         if respond is True:
             log.warning('The "respond" argument of run_javascript() has been removed. '

+ 5 - 2
nicegui/element.py

@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional,
 from typing_extensions import Self
 
 from . import context, core, events, json, outbox, storage
-from .awaitable_response import AwaitableResponse
+from .awaitable_response import AwaitableResponse, NullResponse
 from .dependencies import Component, Library, register_library, register_vue_component
 from .elements.mixins.visibility import Visibility
 from .event_listener import EventListener
@@ -406,11 +406,14 @@ class Element(Visibility):
     def run_method(self, name: str, *args: Any) -> AwaitableResponse:
         """Run a method on the client side.
 
+        If the function is awaited, the result of the method call is returned.
+        Otherwise, the method is executed without waiting for a response.
+
         :param name: name of the method
         :param args: arguments to pass to the method
         """
         if not core.loop:
-            return AwaitableResponse(None, None)
+            return NullResponse()
         return self.client.run_javascript(f'return runMethod({self.id}, "{name}", {json.dumps(args)})')
 
     def _collect_descendants(self, *, include_self: bool = False) -> List[Element]:

+ 11 - 1
nicegui/elements/aggrid.py

@@ -25,7 +25,7 @@ class AgGrid(Element, component='aggrid.js', libraries=['lib/aggrid/ag-grid-comm
 
         An element to create a grid using `AG Grid <https://www.ag-grid.com/>`_.
 
-        The `call_api_method` method can be used to call an AG Grid API method.
+        The methods `call_api_method` and `call_column_api_method` can be used to interact with the AG Grid instance on the client.
 
         :param options: dictionary of AG Grid options
         :param html_columns: list of columns that should be rendered as HTML (default: `[]`)
@@ -89,8 +89,13 @@ class AgGrid(Element, component='aggrid.js', libraries=['lib/aggrid/ag-grid-comm
 
         See `AG Grid API <https://www.ag-grid.com/javascript-data-grid/grid-api/>`_ for a list of methods.
 
+        If the function is awaited, the result of the method call is returned.
+        Otherwise, the method is executed without waiting for a response.
+
         :param name: name of the method
         :param args: arguments to pass to the method
+
+        :return: AwaitableResponse that can be awaited to get the result of the method call
         """
         return self.run_method('call_api_method', name, *args)
 
@@ -99,8 +104,13 @@ class AgGrid(Element, component='aggrid.js', libraries=['lib/aggrid/ag-grid-comm
 
         See `AG Grid Column API <https://www.ag-grid.com/javascript-data-grid/column-api/>`_ for a list of methods.
 
+        If the function is awaited, the result of the method call is returned.
+        Otherwise, the method is executed without waiting for a response.
+
         :param name: name of the method
         :param args: arguments to pass to the method
+
+        :return: AwaitableResponse that can be awaited to get the result of the method call
         """
         return self.run_method('call_column_api_method', name, *args)
 

+ 2 - 2
nicegui/elements/scene.py

@@ -4,7 +4,7 @@ from typing import Any, Callable, Dict, List, Optional, Union
 from typing_extensions import Self
 
 from .. import binding
-from ..awaitable_response import AwaitableResponse
+from ..awaitable_response import AwaitableResponse, NullResponse
 from ..dataclasses import KWONLY_SLOTS
 from ..element import Element
 from ..events import (GenericEventArguments, SceneClickEventArguments, SceneClickHit, SceneDragEventArguments,
@@ -124,7 +124,7 @@ class Scene(Element,
         :param args: arguments to pass to the method
         """
         if not self.is_initialized:
-            return AwaitableResponse(None, None)
+            return NullResponse()
         return super().run_method(name, *args)
 
     def _handle_click(self, e: GenericEventArguments) -> None:

+ 4 - 2
nicegui/functions/javascript.py

@@ -11,15 +11,17 @@ def run_javascript(code: str, *,
     """Run JavaScript
 
     This function runs arbitrary JavaScript code on a page that is executed in the browser.
-    The asynchronous function will return after the command(s) are executed.
     The client must be connected before this function is called.
     To access a client-side object by ID, use the JavaScript function `getElement()`.
 
+    If the function is awaited, the result of the JavaScript code is returned.
+    Otherwise, the JavaScript code is executed without waiting for a response.
+
     :param code: JavaScript code to run
     :param timeout: timeout in seconds (default: `1.0`)
     :param check_interval: interval in seconds to check for a response (default: `0.01`)
 
-    :return: response from the browser, or `None` if `respond` is `False`
+    :return: AwaitableResponse that can be awaited to get the result of the JavaScript code
     """
     if respond is True:
         log.warning('The "respond" argument of run_javascript() has been removed. '

+ 45 - 0
tests/test_awaitable_response.py

@@ -0,0 +1,45 @@
+import asyncio
+
+import pytest
+
+from nicegui import core
+from nicegui.awaitable_response import AwaitableResponse, NullResponse
+
+
+async def test_awaitable_response():
+    core.loop = asyncio.get_event_loop()
+    actions = []
+
+    def do_something() -> AwaitableResponse:
+        def fire_and_forget():
+            actions.append('fire_and_forget')
+
+        async def wait_for_result() -> str:
+            actions.append('wait_for_result')
+            return 'result'
+
+        return AwaitableResponse(fire_and_forget, wait_for_result)
+
+    actions.clear()
+    do_something()
+    await asyncio.sleep(0.1)
+    assert actions == ['fire_and_forget']
+
+    actions.clear()
+    result = await do_something()
+    assert result == 'result'
+    assert actions == ['wait_for_result']
+
+    actions.clear()
+    result = do_something()
+    await asyncio.sleep(0.1)
+    with pytest.raises(RuntimeError, match='AwaitableResponse must be awaited immediately after creation or not at all'):
+        await result
+    assert actions == ['fire_and_forget']
+
+
+async def test_null_response():
+    NullResponse()
+
+    core.loop = asyncio.get_event_loop()
+    await NullResponse()