diff --git a/nicegui/client.py b/nicegui/client.py index 98d1dcc96..dbe153d7d 100644 --- a/nicegui/client.py +++ b/nicegui/client.py @@ -19,6 +19,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 .observables import ObservableDict from .outbox import Outbox @@ -67,8 +68,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 = '' @@ -181,7 +180,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. @@ -192,7 +193,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 """ @@ -204,6 +204,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 @@ -213,12 +217,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) @@ -277,7 +276,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.""" diff --git a/nicegui/element.py b/nicegui/element.py index 9b83e3320..833dcbc4f 100644 --- a/nicegui/element.py +++ b/nicegui/element.py @@ -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() diff --git a/nicegui/elements/aggrid.py b/nicegui/elements/aggrid.py index 6b4817cfa..c819da081 100644 --- a/nicegui/elements/aggrid.py +++ b/nicegui/elements/aggrid.py @@ -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 """ @@ -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 """ @@ -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 """ @@ -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''' diff --git a/nicegui/elements/echart.py b/nicegui/elements/echart.py index ef76806ef..6ac19bd9e 100644 --- a/nicegui/elements/echart.py +++ b/nicegui/elements/echart.py @@ -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 """ diff --git a/nicegui/elements/json_editor.py b/nicegui/elements/json_editor.py index 50257522c..2f3383396 100644 --- a/nicegui/elements/json_editor.py +++ b/nicegui/elements/json_editor.py @@ -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 """ diff --git a/nicegui/elements/leaflet.js b/nicegui/elements/leaflet.js index 2bd5b41d1..2a81d6535 100644 --- a/nicegui/elements/leaflet.js +++ b/nicegui/elements/leaflet.js @@ -107,13 +107,10 @@ export default { clearInterval(connectInterval); }, 100); }, + updated() { + this.map?.setView(this.center, this.zoom); + }, methods: { - setCenter(center) { - this.map.panTo(center); - }, - setZoom(zoom) { - this.map.setZoom(zoom); - }, add_layer(layer, id) { const l = L[layer.type](...layer.args); l.id = id; diff --git a/nicegui/elements/leaflet.py b/nicegui/elements/leaflet.py index 750e9e17e..91089289a 100644 --- a/nicegui/elements/leaflet.py +++ b/nicegui/elements/leaflet.py @@ -98,14 +98,14 @@ def set_center(self, center: Tuple[float, float]) -> None: if self._props['center'] == center: return self._props['center'] = center - self.run_method('setCenter', center) + self.update() def set_zoom(self, zoom: int) -> None: """Set the zoom level of the map.""" if self._props['zoom'] == zoom: return self._props['zoom'] = zoom - self.run_method('setZoom', zoom) + self.update() def remove_layer(self, layer: Layer) -> None: """Remove a layer from the map.""" @@ -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 """ @@ -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 """ diff --git a/nicegui/elements/leaflet_layer.py b/nicegui/elements/leaflet_layer.py index b12c13ec2..d7a326f67 100644 --- a/nicegui/elements/leaflet_layer.py +++ b/nicegui/elements/leaflet_layer.py @@ -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 """ diff --git a/nicegui/elements/markdown.js b/nicegui/elements/markdown.js index 24bcb9376..b5a28bc7a 100644 --- a/nicegui/elements/markdown.js +++ b/nicegui/elements/markdown.js @@ -4,7 +4,7 @@ export default { this.ensure_codehilite_css(); if (this.use_mermaid) { this.mermaid = (await import("mermaid")).default; - this.update(this.$el.innerHTML); + this.renderMermaid(); } }, data() { @@ -12,9 +12,11 @@ export default { mermaid: null, }; }, + updated() { + this.renderMermaid(); + }, methods: { - update(content) { - this.$el.innerHTML = content; + renderMermaid() { this.$el.querySelectorAll(".mermaid-pre").forEach(async (pre, i) => { await this.mermaid.run({ nodes: [pre.children[0]] }); }); diff --git a/nicegui/elements/markdown.py b/nicegui/elements/markdown.py index 029782d72..faeac23ee 100644 --- a/nicegui/elements/markdown.py +++ b/nicegui/elements/markdown.py @@ -34,7 +34,7 @@ def _handle_content_change(self, content: str) -> None: html = prepare_content(content, extras=' '.join(self.extras)) if self._props.get('innerHTML') != html: self._props['innerHTML'] = html - self.run_method('update', html) + self.update() @lru_cache(maxsize=int(os.environ.get('MARKDOWN_CONTENT_CACHE_SIZE', '1000'))) diff --git a/nicegui/elements/restructured_text.py b/nicegui/elements/restructured_text.py index cde94d7de..f941b9654 100644 --- a/nicegui/elements/restructured_text.py +++ b/nicegui/elements/restructured_text.py @@ -21,7 +21,7 @@ def _handle_content_change(self, content: str) -> None: html = prepare_content(content) if self._props.get('innerHTML') != html: self._props['innerHTML'] = html - self.run_method('update', html) + self.update() @lru_cache(maxsize=int(os.environ.get('RST_CONTENT_CACHE_SIZE', '1000'))) diff --git a/nicegui/functions/javascript.py b/nicegui/functions/javascript.py index 624d78c79..2466f890a 100644 --- a/nicegui/functions/javascript.py +++ b/nicegui/functions/javascript.py @@ -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 @@ -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) diff --git a/nicegui/javascript_request.py b/nicegui/javascript_request.py new file mode 100644 index 000000000..2a9515eb8 --- /dev/null +++ b/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) diff --git a/nicegui/native/native_mode.py b/nicegui/native/native_mode.py index 0787bd0d2..42085227e 100644 --- a/nicegui/native/native_mode.py +++ b/nicegui/native/native_mode.py @@ -87,7 +87,7 @@ def window_method_executor() -> None: else: log.error(f'window.{method_name} is not callable') except queue.Empty: - time.sleep(0.01) + time.sleep(0.016) # NOTE: avoid issue https://github.com/zauberzeug/nicegui/issues/2482 on Windows except Exception: log.exception(f'error in window.{method_name}') diff --git a/website/documentation/content/section_audiovisual_elements.py b/website/documentation/content/section_audiovisual_elements.py index 096ec11a6..f2ad48f1d 100644 --- a/website/documentation/content/section_audiovisual_elements.py +++ b/website/documentation/content/section_audiovisual_elements.py @@ -21,9 +21,9 @@ def captions_and_overlays_demo(): with ui.image('https://cdn.stocksnap.io/img-thumbs/960w/airplane-sky_DYPWDEEILG.jpg'): ui.html(''' - + - ''').classes('bg-transparent') + ''').classes('w-full bg-transparent') doc.intro(interactive_image_documentation)