1
0
Эх сурвалжийг харах

Merge branch 'main' into run-with-lifespan

Falko Schindler 1 жил өмнө
parent
commit
8e40424024

+ 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)
 

+ 6 - 0
nicegui/elements/input.py

@@ -29,6 +29,12 @@ class Input(ValidationElement, DisableableElement, component='input.js'):
         You can use the `validation` parameter to define a dictionary of validation rules.
         The key of the first rule that fails will be displayed as an error message.
 
+        Note about styling the input:
+        Quasar's `QInput` component is a wrapper around a native `input` element.
+        This means that you cannot style the input directly,
+        but you can use the `input-class` and `input-style` props to style the native input element.
+        See the "Style" props section on the `QInput <https://quasar.dev/vue-components/input>`_ documentation for more details.
+
         :param label: displayed label for the text input
         :param placeholder: text to show if no value is entered
         :param value: the current value of the text input

+ 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()

+ 29 - 0
website/more_documentation/table_documentation.py

@@ -244,3 +244,32 @@ def more() -> None:
             {'name': 'Christopher'},
         ]
         ui.table(columns=columns, rows=rows, row_key='name')
+
+    @text_demo('Conditional formatting', '''
+        You can use scoped slots to conditionally format the content of a cell.
+        See the [Quasar documentation](https://quasar.dev/vue-components/table#example--body-cell-slot)
+        for more information about body-cell slots.
+        
+        In this demo we use a `q-badge` to display the age in red if the person is under 21 years old.
+        We use the `body-cell-age` slot to insert the `q-badge` into the `age` column.
+        The ":color" attribute of the `q-badge` is set to "red" if the age is under 21, otherwise it is set to "green".
+        The colon in front of the "color" attribute indicates that the value is a JavaScript expression.
+    ''')
+    def conditional_formatting():
+        columns = [
+            {'name': 'name', 'label': 'Name', 'field': 'name'},
+            {'name': 'age', 'label': 'Age', 'field': 'age'},
+        ]
+        rows = [
+            {'name': 'Alice', 'age': 18},
+            {'name': 'Bob', 'age': 21},
+            {'name': 'Carol', 'age': 42},
+        ]
+        table = ui.table(columns=columns, rows=rows, row_key='name')
+        table.add_slot('body-cell-age', '''
+            <q-td key="age" :props="props">
+                <q-badge :color="props.value < 21 ? 'red' : 'green'">
+                    {{ props.value }}
+                </q-badge>
+            </q-td>
+        ''')