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

+ 28 - 11
nicegui/awaitable_response.py

@@ -1,31 +1,48 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
-from asyncio import Task
-from typing import Callable, Optional
+from typing import Callable
 
 
 from . import background_tasks
 from . import background_tasks
 
 
 
 
 class AwaitableResponse:
 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
         """Awaitable Response
 
 
         This class can be used to run one of two different callables, depending on whether the response is awaited or not.
         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 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.
         :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.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):
     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__()
         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.
         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(...)`.
         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:
         if respond is True:
             log.warning('The "respond" argument of run_javascript() has been removed. '
             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 typing_extensions import Self
 
 
 from . import context, core, events, json, outbox, storage
 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 .dependencies import Component, Library, register_library, register_vue_component
 from .elements.mixins.visibility import Visibility
 from .elements.mixins.visibility import Visibility
 from .event_listener import EventListener
 from .event_listener import EventListener
@@ -406,11 +406,14 @@ class Element(Visibility):
     def run_method(self, name: str, *args: Any) -> AwaitableResponse:
     def run_method(self, name: str, *args: Any) -> AwaitableResponse:
         """Run a method on the client side.
         """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 name: name of the method
         :param args: arguments to pass to the method
         :param args: arguments to pass to the method
         """
         """
         if not core.loop:
         if not core.loop:
-            return AwaitableResponse(None, None)
+            return NullResponse()
         return self.client.run_javascript(f'return runMethod({self.id}, "{name}", {json.dumps(args)})')
         return self.client.run_javascript(f'return runMethod({self.id}, "{name}", {json.dumps(args)})')
 
 
     def _collect_descendants(self, *, include_self: bool = False) -> List[Element]:
     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/>`_.
         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 options: dictionary of AG Grid options
         :param html_columns: list of columns that should be rendered as HTML (default: `[]`)
         :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.
         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 name: name of the method
         :param args: arguments to pass to 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)
         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.
         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 name: name of the method
         :param args: arguments to pass to 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)
         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.
         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.
         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 label: displayed label for the text input
         :param placeholder: text to show if no value is entered
         :param placeholder: text to show if no value is entered
         :param value: the current value of the text input
         :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 typing_extensions import Self
 
 
 from .. import binding
 from .. import binding
-from ..awaitable_response import AwaitableResponse
+from ..awaitable_response import AwaitableResponse, NullResponse
 from ..dataclasses import KWONLY_SLOTS
 from ..dataclasses import KWONLY_SLOTS
 from ..element import Element
 from ..element import Element
 from ..events import (GenericEventArguments, SceneClickEventArguments, SceneClickHit, SceneDragEventArguments,
 from ..events import (GenericEventArguments, SceneClickEventArguments, SceneClickHit, SceneDragEventArguments,
@@ -124,7 +124,7 @@ class Scene(Element,
         :param args: arguments to pass to the method
         :param args: arguments to pass to the method
         """
         """
         if not self.is_initialized:
         if not self.is_initialized:
-            return AwaitableResponse(None, None)
+            return NullResponse()
         return super().run_method(name, *args)
         return super().run_method(name, *args)
 
 
     def _handle_click(self, e: GenericEventArguments) -> None:
     def _handle_click(self, e: GenericEventArguments) -> None:

+ 4 - 2
nicegui/functions/javascript.py

@@ -11,15 +11,17 @@ def run_javascript(code: str, *,
     """Run JavaScript
     """Run JavaScript
 
 
     This function runs arbitrary JavaScript code on a page that is executed in the browser.
     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.
     The client must be connected before this function is called.
     To access a client-side object by ID, use the JavaScript function `getElement()`.
     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 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`)
     :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:
     if respond is True:
         log.warning('The "respond" argument of run_javascript() has been removed. '
         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'},
             {'name': 'Christopher'},
         ]
         ]
         ui.table(columns=columns, rows=rows, row_key='name')
         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>
+        ''')