Browse Source

replace check_interval for JavaScript requests with asyncio event (see #2482) (#2827)

Falko Schindler 1 year ago
parent
commit
ea59b943fa

+ 10 - 11
nicegui/client.py

@@ -18,6 +18,7 @@ from .awaitable_response import AwaitableResponse
 from .dependencies import generate_resources
 from .dependencies import generate_resources
 from .element import Element
 from .element import Element
 from .favicon import get_favicon_url
 from .favicon import get_favicon_url
+from .javascript_request import JavaScriptRequest
 from .logging import log
 from .logging import log
 from .outbox import Outbox
 from .outbox import Outbox
 from .version import __version__
 from .version import __version__
@@ -65,8 +66,6 @@ class Client:
                 with Element('q-page'):
                 with Element('q-page'):
                     self.content = Element('div').classes('nicegui-content')
                     self.content = Element('div').classes('nicegui-content')
 
 
-        self.waiting_javascript_commands: Dict[str, Any] = {}
-
         self.title: Optional[str] = None
         self.title: Optional[str] = None
 
 
         self._head_html = ''
         self._head_html = ''
@@ -173,7 +172,9 @@ class Client:
 
 
     def run_javascript(self, code: str, *,
     def run_javascript(self, code: str, *,
                        respond: Optional[bool] = None,  # DEPRECATED
                        respond: Optional[bool] = None,  # DEPRECATED
-                       timeout: float = 1.0, check_interval: float = 0.01) -> AwaitableResponse:
+                       timeout: float = 1.0,
+                       check_interval: float = 0.01,  # DEPRECATED
+                       ) -> AwaitableResponse:
         """Execute JavaScript on the client.
         """Execute JavaScript on the client.
 
 
         The client connection must be established before this method is called.
         The client connection must be established before this method is called.
@@ -184,7 +185,6 @@ class Client:
 
 
         :param code: JavaScript code to run
         :param code: JavaScript code to run
         :param timeout: timeout in seconds (default: `1.0`)
         :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
         :return: AwaitableResponse that can be awaited to get the result of the JavaScript code
         """
         """
@@ -196,6 +196,10 @@ class Client:
             raise ValueError('The "respond" argument of run_javascript() has been removed. '
             raise ValueError('The "respond" argument of run_javascript() has been removed. '
                              'Now the method always returns an AwaitableResponse that can be awaited. '
                              'Now the method always returns an AwaitableResponse that can be awaited. '
                              'Please remove the "respond=False" argument and call the method without awaiting.')
                              'Please remove the "respond=False" argument and call the method without awaiting.')
+        if check_interval != 0.01:
+            log.warning('The "check_interval" argument of run_javascript() and similar methods has been removed. '
+                        'Now the method automatically returns when receiving a response without checking regularly in an interval. '
+                        'Please remove the "check_interval" argument.')
 
 
         request_id = str(uuid.uuid4())
         request_id = str(uuid.uuid4())
         target_id = self._temporary_socket_id or self.id
         target_id = self._temporary_socket_id or self.id
@@ -205,12 +209,7 @@ class Client:
 
 
         async def send_and_wait():
         async def send_and_wait():
             self.outbox.enqueue_message('run_javascript', {'code': code, 'request_id': request_id}, target_id)
             self.outbox.enqueue_message('run_javascript', {'code': code, 'request_id': request_id}, target_id)
-            deadline = time.time() + timeout
-            while request_id not in self.waiting_javascript_commands:
-                if time.time() > deadline:
-                    raise TimeoutError(f'JavaScript did not respond within {timeout:.1f} s')
-                await asyncio.sleep(check_interval)
-            return self.waiting_javascript_commands.pop(request_id)
+            return await JavaScriptRequest(request_id, timeout=timeout)
 
 
         return AwaitableResponse(send_and_forget, send_and_wait)
         return AwaitableResponse(send_and_forget, send_and_wait)
 
 
@@ -269,7 +268,7 @@ class Client:
 
 
     def handle_javascript_response(self, msg: Dict) -> None:
     def handle_javascript_response(self, msg: Dict) -> None:
         """Store the result of a JavaScript command."""
         """Store the result of a JavaScript command."""
-        self.waiting_javascript_commands[msg['request_id']] = msg['result']
+        JavaScriptRequest.resolve(msg['request_id'], msg['result'])
 
 
     def safe_invoke(self, func: Union[Callable[..., Any], Awaitable]) -> None:
     def safe_invoke(self, func: Union[Callable[..., Any], Awaitable]) -> None:
         """Invoke the potentially async function in the client context and catch any exceptions."""
         """Invoke the potentially async function in the client context and catch any exceptions."""

+ 0 - 1
nicegui/element.py

@@ -477,7 +477,6 @@ class Element(Visibility):
         :param name: name of the method
         :param name: name of the method
         :param args: arguments to pass to the method
         :param args: arguments to pass to the method
         :param timeout: maximum time to wait for a response (default: 1 second)
         :param timeout: maximum time to wait for a response (default: 1 second)
-        :param check_interval: time between checks for a response (default: 0.01 seconds)
         """
         """
         if not core.loop:
         if not core.loop:
             return NullResponse()
             return NullResponse()

+ 1 - 4
nicegui/elements/aggrid.py

@@ -105,7 +105,6 @@ class AgGrid(Element, component='aggrid.js', libraries=['lib/aggrid/ag-grid-comm
         :param name: name of the method
         :param name: name of the method
         :param args: arguments to pass to the method
         :param args: arguments to pass to the method
         :param timeout: timeout in seconds (default: 1 second)
         :param timeout: timeout in seconds (default: 1 second)
-        :param check_interval: interval in seconds to check for a response (default: 0.01 seconds)
 
 
         :return: AwaitableResponse that can be awaited to get the result of the method call
         :return: AwaitableResponse that can be awaited to get the result of the method call
         """
         """
@@ -127,7 +126,6 @@ class AgGrid(Element, component='aggrid.js', libraries=['lib/aggrid/ag-grid-comm
         :param name: name of the method
         :param name: name of the method
         :param args: arguments to pass to the method
         :param args: arguments to pass to the method
         :param timeout: timeout in seconds (default: 1 second)
         :param timeout: timeout in seconds (default: 1 second)
-        :param check_interval: interval in seconds to check for a response (default: 0.01 seconds)
 
 
         :return: AwaitableResponse that can be awaited to get the result of the method call
         :return: AwaitableResponse that can be awaited to get the result of the method call
         """
         """
@@ -146,7 +144,6 @@ class AgGrid(Element, component='aggrid.js', libraries=['lib/aggrid/ag-grid-comm
         :param name: name of the method
         :param name: name of the method
         :param args: arguments to pass to the method
         :param args: arguments to pass to the method
         :param timeout: timeout in seconds (default: 1 second)
         :param timeout: timeout in seconds (default: 1 second)
-        :param check_interval: interval in seconds to check for a response (default: 0.01 seconds)
 
 
         :return: AwaitableResponse that can be awaited to get the result of the method call
         :return: AwaitableResponse that can be awaited to get the result of the method call
         """
         """
@@ -185,7 +182,7 @@ class AgGrid(Element, component='aggrid.js', libraries=['lib/aggrid/ag-grid-comm
         This does not happen when the cell loses focus, unless ``stopEditingWhenCellsLoseFocus: True`` is set.
         This does not happen when the cell loses focus, unless ``stopEditingWhenCellsLoseFocus: True`` is set.
 
 
         :param timeout: timeout in seconds (default: 1 second)
         :param timeout: timeout in seconds (default: 1 second)
-        :param check_interval: interval in seconds to check for a response (default: 0.01 seconds)
+
         :return: list of row data
         :return: list of row data
         """
         """
         result = await self.client.run_javascript(f'''
         result = await self.client.run_javascript(f'''

+ 0 - 1
nicegui/elements/echart.py

@@ -109,7 +109,6 @@ class EChart(Element, component='echart.js', libraries=['lib/echarts/echarts.min
         :param name: name of the method (a prefix ":" indicates that the arguments are JavaScript expressions)
         :param name: name of the method (a prefix ":" indicates that the arguments are JavaScript expressions)
         :param args: arguments to pass to the method (Python objects or JavaScript expressions)
         :param args: arguments to pass to the method (Python objects or JavaScript expressions)
         :param timeout: timeout in seconds (default: 1 second)
         :param timeout: timeout in seconds (default: 1 second)
-        :param check_interval: interval in seconds to check for a response (default: 0.01 seconds)
 
 
         :return: AwaitableResponse that can be awaited to get the result of the method call
         :return: AwaitableResponse that can be awaited to get the result of the method call
         """
         """

+ 0 - 1
nicegui/elements/json_editor.py

@@ -68,7 +68,6 @@ class JsonEditor(Element, component='json_editor.js', exposed_libraries=['lib/va
         :param name: name of the method (a prefix ":" indicates that the arguments are JavaScript expressions)
         :param name: name of the method (a prefix ":" indicates that the arguments are JavaScript expressions)
         :param args: arguments to pass to the method (Python objects or JavaScript expressions)
         :param args: arguments to pass to the method (Python objects or JavaScript expressions)
         :param timeout: timeout in seconds (default: 1 second)
         :param timeout: timeout in seconds (default: 1 second)
-        :param check_interval: interval in seconds to check for a response (default: 0.01 seconds)
 
 
         :return: AwaitableResponse that can be awaited to get the result of the method call
         :return: AwaitableResponse that can be awaited to get the result of the method call
         """
         """

+ 0 - 2
nicegui/elements/leaflet.py

@@ -128,7 +128,6 @@ class Leaflet(Element, component='leaflet.js'):
         :param name: name of the method (a prefix ":" indicates that the arguments are JavaScript expressions)
         :param name: name of the method (a prefix ":" indicates that the arguments are JavaScript expressions)
         :param args: arguments to pass to the method
         :param args: arguments to pass to the method
         :param timeout: timeout in seconds (default: 1 second)
         :param timeout: timeout in seconds (default: 1 second)
-        :param check_interval: interval in seconds to check for a response (default: 0.01 seconds)
 
 
         :return: AwaitableResponse that can be awaited to get the result of the method call
         :return: AwaitableResponse that can be awaited to get the result of the method call
         """
         """
@@ -144,7 +143,6 @@ class Leaflet(Element, component='leaflet.js'):
         :param name: name of the method (a prefix ":" indicates that the arguments are JavaScript expressions)
         :param name: name of the method (a prefix ":" indicates that the arguments are JavaScript expressions)
         :param args: arguments to pass to the method
         :param args: arguments to pass to the method
         :param timeout: timeout in seconds (default: 1 second)
         :param timeout: timeout in seconds (default: 1 second)
-        :param check_interval: interval in seconds to check for a response (default: 0.01 seconds)
 
 
         :return: AwaitableResponse that can be awaited to get the result of the method call
         :return: AwaitableResponse that can be awaited to get the result of the method call
         """
         """

+ 0 - 1
nicegui/elements/leaflet_layer.py

@@ -38,7 +38,6 @@ class Layer:
         :param name: name of the method (a prefix ":" indicates that the arguments are JavaScript expressions)
         :param name: name of the method (a prefix ":" indicates that the arguments are JavaScript expressions)
         :param args: arguments to pass to the method
         :param args: arguments to pass to the method
         :param timeout: timeout in seconds (default: 1 second)
         :param timeout: timeout in seconds (default: 1 second)
-        :param check_interval: interval in seconds to check for a response (default: 0.01 seconds)
 
 
         :return: AwaitableResponse that can be awaited to get the result of the method call
         :return: AwaitableResponse that can be awaited to get the result of the method call
         """
         """

+ 2 - 13
nicegui/functions/javascript.py

@@ -2,11 +2,10 @@ from typing import Optional
 
 
 from .. import context
 from .. import context
 from ..awaitable_response import AwaitableResponse
 from ..awaitable_response import AwaitableResponse
-from ..logging import log
 
 
 
 
 def run_javascript(code: str, *,
 def run_javascript(code: str, *,
-                   respond: Optional[bool] = None,  # DEPRECATED
+                   respond: Optional[bool] = None,
                    timeout: float = 1.0, check_interval: float = 0.01) -> AwaitableResponse:
                    timeout: float = 1.0, check_interval: float = 0.01) -> AwaitableResponse:
     """Run JavaScript
     """Run JavaScript
 
 
@@ -19,17 +18,7 @@ def run_javascript(code: str, *,
 
 
     :param code: JavaScript code to run
     :param code: JavaScript code to run
     :param timeout: timeout in seconds (default: `1.0`)
     :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
     :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. '
-                    'Now the function always returns an AwaitableResponse that can be awaited. '
-                    'Please remove the "respond=True" argument.')
-    if respond is False:
-        raise ValueError('The "respond" argument of run_javascript() has been removed. '
-                         'Now the function always returns an AwaitableResponse that can be awaited. '
-                         'Please remove the "respond=False" argument and call the function without awaiting.')
-
-    return context.get_client().run_javascript(code, timeout=timeout, check_interval=check_interval)
+    return context.get_client().run_javascript(code, respond=respond, timeout=timeout, check_interval=check_interval)

+ 32 - 0
nicegui/javascript_request.py

@@ -0,0 +1,32 @@
+from __future__ import annotations
+
+import asyncio
+from typing import Any, Dict
+
+
+class JavaScriptRequest:
+    _instances: Dict[str, JavaScriptRequest] = {}
+
+    def __init__(self, request_id: str, *, timeout: float) -> None:
+        self.request_id = request_id
+        self._instances[request_id] = self
+        self.timeout = timeout
+        self._event = asyncio.Event()
+        self._result: Any = None
+
+    @classmethod
+    def resolve(cls, request_id: str, result: Any) -> None:
+        """Store the result of a JavaScript request and unblock the awaiter."""
+        request = cls._instances[request_id]
+        request._result = result  # pylint: disable=protected-access
+        request._event.set()  # pylint: disable=protected-access
+
+    def __await__(self) -> Any:
+        try:
+            yield from asyncio.wait_for(self._event.wait(), self.timeout).__await__()
+        except asyncio.TimeoutError as e:
+            raise TimeoutError(f'JavaScript did not respond within {self.timeout:.1f} s') from e
+        else:
+            return self._result
+        finally:
+            self._instances.pop(self.request_id)