Skip to content

Commit

Permalink
replace check_interval for JavaScript requests with asyncio event (see
Browse files Browse the repository at this point in the history
  • Loading branch information
falkoschindler committed Apr 5, 2024
1 parent d4c22cd commit 27eb9ae
Show file tree
Hide file tree
Showing 9 changed files with 45 additions and 34 deletions.
21 changes: 10 additions & 11 deletions nicegui/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .dependencies import generate_resources
from .element import Element
from .favicon import get_favicon_url
from .javascript_request import JavaScriptRequest
from .logging import log
from .outbox import Outbox
from .version import __version__
Expand Down Expand Up @@ -65,8 +66,6 @@ def __init__(self, page: page, *, shared: bool = False) -> None:
with Element('q-page'):
self.content = Element('div').classes('nicegui-content')

self.waiting_javascript_commands: Dict[str, Any] = {}

self.title: Optional[str] = None

self._head_html = ''
Expand Down Expand Up @@ -173,7 +172,9 @@ async def disconnected(self, check_interval: float = 0.1) -> None:

def run_javascript(self, code: str, *,
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.
The client connection must be established before this method is called.
Expand All @@ -184,7 +185,6 @@ def run_javascript(self, code: str, *,
: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
"""
Expand All @@ -196,6 +196,10 @@ def run_javascript(self, code: str, *,
raise ValueError('The "respond" argument of run_javascript() has been removed. '
'Now the method always returns an AwaitableResponse that can be awaited. '
'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())
target_id = self._temporary_socket_id or self.id
Expand All @@ -205,12 +209,7 @@ def send_and_forget():

async def send_and_wait():
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)

Expand Down Expand Up @@ -269,7 +268,7 @@ def handle_event(self, msg: Dict) -> None:

def handle_javascript_response(self, msg: Dict) -> None:
"""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:
"""Invoke the potentially async function in the client context and catch any exceptions."""
Expand Down
1 change: 0 additions & 1 deletion nicegui/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,6 @@ def run_method(self, name: str, *args: Any, timeout: float = 1, check_interval:
:param name: name of the method
:param args: arguments to pass to the method
: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:
return NullResponse()
Expand Down
5 changes: 1 addition & 4 deletions nicegui/elements/aggrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ def run_grid_method(self, name: str, *args, timeout: float = 1, check_interval:
:param name: name of the method
:param args: arguments to pass to the method
: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
"""
Expand All @@ -127,7 +126,6 @@ def run_column_method(self, name: str, *args,
:param name: name of the method
:param args: arguments to pass to the method
: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
"""
Expand All @@ -146,7 +144,6 @@ def run_row_method(self, row_id: str, name: str, *args,
:param name: name of the method
:param args: arguments to pass to the method
: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
"""
Expand Down Expand Up @@ -185,7 +182,7 @@ async def get_client_data(self, *, timeout: float = 1, check_interval: float = 0
This does not happen when the cell loses focus, unless ``stopEditingWhenCellsLoseFocus: True`` is set.
: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
"""
result = await self.client.run_javascript(f'''
Expand Down
1 change: 0 additions & 1 deletion nicegui/elements/echart.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ def run_chart_method(self, name: str, *args, timeout: float = 1,
: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 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
"""
Expand Down
1 change: 0 additions & 1 deletion nicegui/elements/json_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ def run_editor_method(self, name: str, *args, timeout: float = 1,
: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 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
"""
Expand Down
2 changes: 0 additions & 2 deletions nicegui/elements/leaflet.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ def run_map_method(self, name: str, *args, timeout: float = 1, check_interval: f
:param name: name of the method (a prefix ":" indicates that the arguments are JavaScript expressions)
:param args: arguments to pass to the method
: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
"""
Expand All @@ -144,7 +143,6 @@ def run_layer_method(self, layer_id: str, name: str, *args, timeout: float = 1,
:param name: name of the method (a prefix ":" indicates that the arguments are JavaScript expressions)
:param args: arguments to pass to the method
: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
"""
Expand Down
1 change: 0 additions & 1 deletion nicegui/elements/leaflet_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ def run_method(self, name: str, *args: Any, timeout: float = 1, check_interval:
:param name: name of the method (a prefix ":" indicates that the arguments are JavaScript expressions)
:param args: arguments to pass to the method
: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
"""
Expand Down
15 changes: 2 additions & 13 deletions nicegui/functions/javascript.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

from .. import context
from ..awaitable_response import AwaitableResponse
from ..logging import log


def run_javascript(code: str, *,
respond: Optional[bool] = None, # DEPRECATED
respond: Optional[bool] = None,
timeout: float = 1.0, check_interval: float = 0.01) -> AwaitableResponse:
"""Run JavaScript
Expand All @@ -19,17 +18,7 @@ def run_javascript(code: str, *,
: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. '
'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 changes: 32 additions & 0 deletions nicegui/javascript_request.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 27eb9ae

Please sign in to comment.