Преглед изворни кода

Merge branch '1.4' into run_javascript

# Conflicts:
#	website/documentation_tools.py
Falko Schindler пре 1 година
родитељ
комит
f34f013bea

+ 1 - 1
development.dockerfile

@@ -1,6 +1,6 @@
 FROM python:3.8-slim
 FROM python:3.8-slim
 
 
-RUN apt update && apt install curl -y
+RUN apt update && apt install curl build-essential -y
 
 
 # We use Poetry for dependency management
 # We use Poetry for dependency management
 RUN curl -sSL https://install.python-poetry.org | python3 - && \
 RUN curl -sSL https://install.python-poetry.org | python3 - && \

+ 3 - 3
fly.toml

@@ -12,7 +12,7 @@ kill_timeout = "5s"
   dockerfile = "fly.dockerfile"
   dockerfile = "fly.dockerfile"
 
 
 [deploy]
 [deploy]
-  strategy = "bluegreen"
+  strategy = "canary"
 
 
 [processes]
 [processes]
   app = ""
   app = ""
@@ -37,8 +37,8 @@ kill_timeout = "5s"
     port = 443
     port = 443
     handlers = ["tls", "http"]
     handlers = ["tls", "http"]
   [services.concurrency]
   [services.concurrency]
-    type = "connections"
-    hard_limit = 80
+    type = "requests"
+    hard_limit = 60
     soft_limit = 30
     soft_limit = 30
 
 
   [[services.tcp_checks]]
   [[services.tcp_checks]]

+ 8 - 0
nicegui/elements/context_menu.py

@@ -13,3 +13,11 @@ class ContextMenu(Element):
         super().__init__('q-menu')
         super().__init__('q-menu')
         self._props['context-menu'] = True
         self._props['context-menu'] = True
         self._props['touch-position'] = True
         self._props['touch-position'] = True
+
+    def open(self) -> None:
+        """Open the context menu."""
+        self.run_method('show')
+
+    def close(self) -> None:
+        """Close the context menu."""
+        self.run_method('hide')

+ 2 - 1
nicegui/elements/menu.py

@@ -4,6 +4,7 @@ from typing_extensions import Self
 
 
 from .. import globals  # pylint: disable=redefined-builtin
 from .. import globals  # pylint: disable=redefined-builtin
 from ..events import ClickEventArguments, handle_event
 from ..events import ClickEventArguments, handle_event
+from .context_menu import ContextMenu
 from .mixins.text_element import TextElement
 from .mixins.text_element import TextElement
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
 
 
@@ -65,6 +66,6 @@ class MenuItem(TextElement):
         def handle_click(_) -> None:
         def handle_click(_) -> None:
             handle_event(on_click, ClickEventArguments(sender=self, client=self.client))
             handle_event(on_click, ClickEventArguments(sender=self, client=self.client))
             if auto_close:
             if auto_close:
-                assert isinstance(self.menu, Menu)
+                assert isinstance(self.menu, (Menu, ContextMenu))
                 self.menu.close()
                 self.menu.close()
         self.on('click', handle_click, [])
         self.on('click', handle_click, [])

+ 1 - 0
nicegui/elements/timer.js

@@ -0,0 +1 @@
+export default {};

+ 9 - 14
nicegui/functions/timer.py → nicegui/elements/timer.py

@@ -4,10 +4,10 @@ from typing import Any, Callable, Optional
 
 
 from .. import background_tasks, globals, helpers  # pylint: disable=redefined-builtin
 from .. import background_tasks, globals, helpers  # pylint: disable=redefined-builtin
 from ..binding import BindableProperty
 from ..binding import BindableProperty
-from ..slot import Slot
+from ..element import Element
 
 
 
 
-class Timer:
+class Timer(Element, component='timer.js'):
     active = BindableProperty()
     active = BindableProperty()
     interval = BindableProperty()
     interval = BindableProperty()
 
 
@@ -28,10 +28,10 @@ class Timer:
         :param active: whether the callback should be executed or not (can be changed during runtime)
         :param active: whether the callback should be executed or not (can be changed during runtime)
         :param once: whether the callback is only executed once after a delay specified by `interval` (default: `False`)
         :param once: whether the callback is only executed once after a delay specified by `interval` (default: `False`)
         """
         """
+        super().__init__()
         self.interval = interval
         self.interval = interval
         self.callback: Optional[Callable[..., Any]] = callback
         self.callback: Optional[Callable[..., Any]] = callback
         self.active = active
         self.active = active
-        self.slot: Optional[Slot] = globals.get_slot()
         self._is_canceled: bool = False
         self._is_canceled: bool = False
 
 
         coroutine = self._run_once if once else self._run_in_loop
         coroutine = self._run_once if once else self._run_in_loop
@@ -57,8 +57,7 @@ class Timer:
         try:
         try:
             if not await self._connected():
             if not await self._connected():
                 return
                 return
-            assert self.slot is not None
-            with self.slot:
+            with self.parent_slot:
                 await asyncio.sleep(self.interval)
                 await asyncio.sleep(self.interval)
                 if self.active and not self._should_stop():
                 if self.active and not self._should_stop():
                     await self._invoke_callback()
                     await self._invoke_callback()
@@ -69,8 +68,7 @@ class Timer:
         try:
         try:
             if not await self._connected():
             if not await self._connected():
                 return
                 return
-            assert self.slot is not None
-            with self.slot:
+            with self.parent_slot:
                 while not self._should_stop():
                 while not self._should_stop():
                     try:
                     try:
                         start = time.time()
                         start = time.time()
@@ -101,27 +99,24 @@ class Timer:
         See https://github.com/zauberzeug/nicegui/issues/206 for details.
         See https://github.com/zauberzeug/nicegui/issues/206 for details.
         Returns True if the client is connected, False if the client is not connected and the timer should be cancelled.
         Returns True if the client is connected, False if the client is not connected and the timer should be cancelled.
         """
         """
-        assert self.slot is not None
-        if self.slot.parent.client.shared:
+        if self.client.shared:
             return True
             return True
 
 
         # ignore served pages which do not reconnect to backend (e.g. monitoring requests, scrapers etc.)
         # ignore served pages which do not reconnect to backend (e.g. monitoring requests, scrapers etc.)
         try:
         try:
-            await self.slot.parent.client.connected(timeout=timeout)
+            await self.client.connected(timeout=timeout)
             return True
             return True
         except TimeoutError:
         except TimeoutError:
             globals.log.error(f'Timer cancelled because client is not connected after {timeout} seconds')
             globals.log.error(f'Timer cancelled because client is not connected after {timeout} seconds')
             return False
             return False
 
 
     def _should_stop(self) -> bool:
     def _should_stop(self) -> bool:
-        assert self.slot is not None
         return (
         return (
-            self.slot.parent.is_deleted or
-            self.slot.parent.client.id not in globals.clients or
+            self.is_deleted or
+            self.client.id not in globals.clients or
             self._is_canceled or
             self._is_canceled or
             globals.state in {globals.State.STOPPING, globals.State.STOPPED}
             globals.state in {globals.State.STOPPING, globals.State.STOPPED}
         )
         )
 
 
     def _cleanup(self) -> None:
     def _cleanup(self) -> None:
-        self.slot = None
         self.callback = None
         self.callback = None

+ 32 - 4
nicegui/functions/refreshable.py

@@ -1,5 +1,7 @@
-from dataclasses import dataclass
-from typing import Any, Awaitable, Callable, Dict, List, Tuple, Union
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import Any, Awaitable, Callable, ClassVar, Dict, List, Optional, Tuple, Union
 
 
 from typing_extensions import Self
 from typing_extensions import Self
 
 
@@ -11,13 +13,20 @@ from ..helpers import is_coroutine_function
 
 
 @dataclass(**KWONLY_SLOTS)
 @dataclass(**KWONLY_SLOTS)
 class RefreshableTarget:
 class RefreshableTarget:
-    container: Element
+    container: RefreshableContainer
+    refreshable: refreshable
     instance: Any
     instance: Any
     args: Tuple[Any, ...]
     args: Tuple[Any, ...]
     kwargs: Dict[str, Any]
     kwargs: Dict[str, Any]
 
 
+    current_target: ClassVar[Optional[RefreshableTarget]] = None
+    locals: List[Any] = field(default_factory=list)
+    next_index: int = 0
+
     def run(self, func: Callable[..., Any]) -> Union[None, Awaitable]:
     def run(self, func: Callable[..., Any]) -> Union[None, Awaitable]:
         """Run the function and return the result."""
         """Run the function and return the result."""
+        RefreshableTarget.current_target = self
+        self.next_index = 0
         # pylint: disable=no-else-return
         # pylint: disable=no-else-return
         if is_coroutine_function(func):
         if is_coroutine_function(func):
             async def wait_for_result() -> None:
             async def wait_for_result() -> None:
@@ -67,7 +76,8 @@ class refreshable:
 
 
     def __call__(self, *args: Any, **kwargs: Any) -> Union[None, Awaitable]:
     def __call__(self, *args: Any, **kwargs: Any) -> Union[None, Awaitable]:
         self.prune()
         self.prune()
-        target = RefreshableTarget(container=RefreshableContainer(), instance=self.instance, args=args, kwargs=kwargs)
+        target = RefreshableTarget(container=RefreshableContainer(), refreshable=self, instance=self.instance,
+                                   args=args, kwargs=kwargs)
         self.targets.append(target)
         self.targets.append(target)
         return target.run(self.func)
         return target.run(self.func)
 
 
@@ -106,3 +116,21 @@ class refreshable:
             for target in self.targets
             for target in self.targets
             if target.container.client.id in globals.clients and target.container.id in target.container.client.elements
             if target.container.client.id in globals.clients and target.container.id in target.container.client.elements
         ]
         ]
+
+
+def state(value: Any) -> Tuple[Any, Callable[[Any], None]]:
+    target = RefreshableTarget.current_target
+    assert target is not None
+
+    if target.next_index >= len(target.locals):
+        target.locals.append(value)
+    else:
+        value = target.locals[target.next_index]
+
+    def set_value(new_value: Any, index=target.next_index) -> None:
+        target.locals[index] = new_value
+        target.refreshable.refresh()
+
+    target.next_index += 1
+
+    return value, set_value

+ 1 - 2
nicegui/storage.py

@@ -1,5 +1,4 @@
 import contextvars
 import contextvars
-import json
 import uuid
 import uuid
 from collections.abc import MutableMapping
 from collections.abc import MutableMapping
 from pathlib import Path
 from pathlib import Path
@@ -12,7 +11,7 @@ from starlette.middleware.sessions import SessionMiddleware
 from starlette.requests import Request
 from starlette.requests import Request
 from starlette.responses import Response
 from starlette.responses import Response
 
 
-from . import background_tasks, globals, observables  # pylint: disable=redefined-builtin
+from . import background_tasks, globals, json, observables  # pylint: disable=redefined-builtin
 
 
 request_contextvar: contextvars.ContextVar[Optional[Request]] = contextvars.ContextVar('request_var', default=None)
 request_contextvar: contextvars.ContextVar[Optional[Request]] = contextvars.ContextVar('request_var', default=None)
 
 

+ 4 - 3
nicegui/ui.py

@@ -70,6 +70,7 @@ __all__ = [
     'tabs',
     'tabs',
     'textarea',
     'textarea',
     'time',
     'time',
+    'timer',
     'timeline',
     'timeline',
     'timeline_entry',
     'timeline_entry',
     'toggle',
     'toggle',
@@ -84,7 +85,7 @@ __all__ = [
     'notify',
     'notify',
     'open',
     'open',
     'refreshable',
     'refreshable',
-    'timer',
+    'state',
     'update',
     'update',
     'page',
     'page',
     'drawer',
     'drawer',
@@ -170,6 +171,7 @@ from .elements.textarea import Textarea as textarea
 from .elements.time import Time as time
 from .elements.time import Time as time
 from .elements.timeline import Timeline as timeline
 from .elements.timeline import Timeline as timeline
 from .elements.timeline import TimelineEntry as timeline_entry
 from .elements.timeline import TimelineEntry as timeline_entry
+from .elements.timer import Timer as timer
 from .elements.toggle import Toggle as toggle
 from .elements.toggle import Toggle as toggle
 from .elements.tooltip import Tooltip as tooltip
 from .elements.tooltip import Tooltip as tooltip
 from .elements.tree import Tree as tree
 from .elements.tree import Tree as tree
@@ -180,8 +182,7 @@ from .functions.html import add_body_html, add_head_html
 from .functions.javascript import run_javascript
 from .functions.javascript import run_javascript
 from .functions.notify import notify
 from .functions.notify import notify
 from .functions.open import open  # pylint: disable=redefined-builtin
 from .functions.open import open  # pylint: disable=redefined-builtin
-from .functions.refreshable import refreshable
-from .functions.timer import Timer as timer
+from .functions.refreshable import refreshable, state
 from .functions.update import update
 from .functions.update import update
 from .page import page
 from .page import page
 from .page_layout import Drawer as drawer
 from .page_layout import Drawer as drawer

+ 55 - 54
poetry.lock

@@ -878,13 +878,13 @@ files = [
 
 
 [[package]]
 [[package]]
 name = "httpcore"
 name = "httpcore"
-version = "0.17.3"
+version = "0.18.0"
 description = "A minimal low-level HTTP client."
 description = "A minimal low-level HTTP client."
 optional = false
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
 files = [
-    {file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"},
-    {file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"},
+    {file = "httpcore-0.18.0-py3-none-any.whl", hash = "sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced"},
+    {file = "httpcore-0.18.0.tar.gz", hash = "sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9"},
 ]
 ]
 
 
 [package.dependencies]
 [package.dependencies]
@@ -899,46 +899,47 @@ socks = ["socksio (==1.*)"]
 
 
 [[package]]
 [[package]]
 name = "httptools"
 name = "httptools"
-version = "0.6.0"
+version = "0.6.1"
 description = "A collection of framework independent HTTP protocol utils."
 description = "A collection of framework independent HTTP protocol utils."
 optional = false
 optional = false
-python-versions = ">=3.5.0"
-files = [
-    {file = "httptools-0.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:818325afee467d483bfab1647a72054246d29f9053fd17cc4b86cda09cc60339"},
-    {file = "httptools-0.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72205730bf1be875003692ca54a4a7c35fac77b4746008966061d9d41a61b0f5"},
-    {file = "httptools-0.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33eb1d4e609c835966e969a31b1dedf5ba16b38cab356c2ce4f3e33ffa94cad3"},
-    {file = "httptools-0.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdc6675ec6cb79d27e0575750ac6e2b47032742e24eed011b8db73f2da9ed40"},
-    {file = "httptools-0.6.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:463c3bc5ef64b9cf091be9ac0e0556199503f6e80456b790a917774a616aff6e"},
-    {file = "httptools-0.6.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82f228b88b0e8c6099a9c4757ce9fdbb8b45548074f8d0b1f0fc071e35655d1c"},
-    {file = "httptools-0.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:0781fedc610293a2716bc7fa142d4c85e6776bc59d617a807ff91246a95dea35"},
-    {file = "httptools-0.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:721e503245d591527cddd0f6fd771d156c509e831caa7a57929b55ac91ee2b51"},
-    {file = "httptools-0.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:274bf20eeb41b0956e34f6a81f84d26ed57c84dd9253f13dcb7174b27ccd8aaf"},
-    {file = "httptools-0.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:259920bbae18740a40236807915def554132ad70af5067e562f4660b62c59b90"},
-    {file = "httptools-0.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03bfd2ae8a2d532952ac54445a2fb2504c804135ed28b53fefaf03d3a93eb1fd"},
-    {file = "httptools-0.6.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f959e4770b3fc8ee4dbc3578fd910fab9003e093f20ac8c621452c4d62e517cb"},
-    {file = "httptools-0.6.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6e22896b42b95b3237eccc42278cd72c0df6f23247d886b7ded3163452481e38"},
-    {file = "httptools-0.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:38f3cafedd6aa20ae05f81f2e616ea6f92116c8a0f8dcb79dc798df3356836e2"},
-    {file = "httptools-0.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:47043a6e0ea753f006a9d0dd076a8f8c99bc0ecae86a0888448eb3076c43d717"},
-    {file = "httptools-0.6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a541579bed0270d1ac10245a3e71e5beeb1903b5fbbc8d8b4d4e728d48ff1d"},
-    {file = "httptools-0.6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65d802e7b2538a9756df5acc062300c160907b02e15ed15ba035b02bce43e89c"},
-    {file = "httptools-0.6.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:26326e0a8fe56829f3af483200d914a7cd16d8d398d14e36888b56de30bec81a"},
-    {file = "httptools-0.6.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e41ccac9e77cd045f3e4ee0fc62cbf3d54d7d4b375431eb855561f26ee7a9ec4"},
-    {file = "httptools-0.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4e748fc0d5c4a629988ef50ac1aef99dfb5e8996583a73a717fc2cac4ab89932"},
-    {file = "httptools-0.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cf8169e839a0d740f3d3c9c4fa630ac1a5aaf81641a34575ca6773ed7ce041a1"},
-    {file = "httptools-0.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5dcc14c090ab57b35908d4a4585ec5c0715439df07be2913405991dbb37e049d"},
-    {file = "httptools-0.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0b0571806a5168013b8c3d180d9f9d6997365a4212cb18ea20df18b938aa0b"},
-    {file = "httptools-0.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fb4a608c631f7dcbdf986f40af7a030521a10ba6bc3d36b28c1dc9e9035a3c0"},
-    {file = "httptools-0.6.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:93f89975465133619aea8b1952bc6fa0e6bad22a447c6d982fc338fbb4c89649"},
-    {file = "httptools-0.6.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:73e9d66a5a28b2d5d9fbd9e197a31edd02be310186db423b28e6052472dc8201"},
-    {file = "httptools-0.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:22c01fcd53648162730a71c42842f73b50f989daae36534c818b3f5050b54589"},
-    {file = "httptools-0.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f96d2a351b5625a9fd9133c95744e8ca06f7a4f8f0b8231e4bbaae2c485046a"},
-    {file = "httptools-0.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72ec7c70bd9f95ef1083d14a755f321d181f046ca685b6358676737a5fecd26a"},
-    {file = "httptools-0.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b703d15dbe082cc23266bf5d9448e764c7cb3fcfe7cb358d79d3fd8248673ef9"},
-    {file = "httptools-0.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82c723ed5982f8ead00f8e7605c53e55ffe47c47465d878305ebe0082b6a1755"},
-    {file = "httptools-0.6.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b0a816bb425c116a160fbc6f34cece097fd22ece15059d68932af686520966bd"},
-    {file = "httptools-0.6.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:dea66d94e5a3f68c5e9d86e0894653b87d952e624845e0b0e3ad1c733c6cc75d"},
-    {file = "httptools-0.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:23b09537086a5a611fad5696fc8963d67c7e7f98cb329d38ee114d588b0b74cd"},
-    {file = "httptools-0.6.0.tar.gz", hash = "sha256:9fc6e409ad38cbd68b177cd5158fc4042c796b82ca88d99ec78f07bed6c6b796"},
+python-versions = ">=3.8.0"
+files = [
+    {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"},
+    {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"},
+    {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"},
+    {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"},
+    {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"},
+    {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"},
+    {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"},
+    {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"},
+    {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"},
+    {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"},
+    {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"},
+    {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"},
+    {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"},
+    {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"},
+    {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"},
+    {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"},
+    {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"},
+    {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"},
+    {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"},
+    {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"},
+    {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"},
+    {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"},
+    {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"},
+    {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"},
+    {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"},
+    {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"},
+    {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"},
+    {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"},
+    {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"},
+    {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"},
+    {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"},
+    {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"},
+    {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"},
+    {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"},
+    {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"},
+    {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"},
 ]
 ]
 
 
 [package.extras]
 [package.extras]
@@ -946,18 +947,18 @@ test = ["Cython (>=0.29.24,<0.30.0)"]
 
 
 [[package]]
 [[package]]
 name = "httpx"
 name = "httpx"
-version = "0.24.1"
+version = "0.25.0"
 description = "The next generation HTTP client."
 description = "The next generation HTTP client."
 optional = false
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
 files = [
-    {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"},
-    {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"},
+    {file = "httpx-0.25.0-py3-none-any.whl", hash = "sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100"},
+    {file = "httpx-0.25.0.tar.gz", hash = "sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875"},
 ]
 ]
 
 
 [package.dependencies]
 [package.dependencies]
 certifi = "*"
 certifi = "*"
-httpcore = ">=0.15.0,<0.18.0"
+httpcore = ">=0.18.0,<0.19.0"
 idna = "*"
 idna = "*"
 sniffio = "*"
 sniffio = "*"
 
 
@@ -1546,13 +1547,13 @@ files = [
 
 
 [[package]]
 [[package]]
 name = "outcome"
 name = "outcome"
-version = "1.2.0"
+version = "1.3.0"
 description = "Capture the outcome of Python function calls."
 description = "Capture the outcome of Python function calls."
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
-    {file = "outcome-1.2.0-py2.py3-none-any.whl", hash = "sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5"},
-    {file = "outcome-1.2.0.tar.gz", hash = "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672"},
+    {file = "outcome-1.3.0-py2.py3-none-any.whl", hash = "sha256:7b688fd82db72f4b0bc9e883a00359d4d4179cd97d27f09c9644d0c842ba7786"},
+    {file = "outcome-1.3.0.tar.gz", hash = "sha256:588ef4dc10b64e8df160d8d1310c44e1927129a66d6d2ef86845cef512c5f24c"},
 ]
 ]
 
 
 [package.dependencies]
 [package.dependencies]
@@ -2637,13 +2638,13 @@ files = [
 
 
 [[package]]
 [[package]]
 name = "urllib3"
 name = "urllib3"
-version = "2.0.6"
+version = "2.0.7"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
 description = "HTTP library with thread-safe connection pooling, file post, and more."
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
-    {file = "urllib3-2.0.6-py3-none-any.whl", hash = "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2"},
-    {file = "urllib3-2.0.6.tar.gz", hash = "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564"},
+    {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"},
+    {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"},
 ]
 ]
 
 
 [package.dependencies]
 [package.dependencies]
@@ -3050,4 +3051,4 @@ plotly = ["plotly"]
 [metadata]
 [metadata]
 lock-version = "2.0"
 lock-version = "2.0"
 python-versions = "^3.8"
 python-versions = "^3.8"
-content-hash = "5c7522bb23fb885df956bc26eb00c337467119f635001ecc8d783749eb5bc019"
+content-hash = "f0a0ab5d0551c62190399771521b47c79dc54c8e8ddc310d3e6b0a1117072d73"

+ 2 - 2
pyproject.toml

@@ -28,7 +28,7 @@ pywebview = { version = "^4.0.2", optional = true }
 plotly = { version = "^5.13.0", optional = true }
 plotly = { version = "^5.13.0", optional = true }
 matplotlib = { version = "^3.5.0", optional = true }
 matplotlib = { version = "^3.5.0", optional = true }
 netifaces = { version = "^0.11.0", optional = true }
 netifaces = { version = "^0.11.0", optional = true }
-httpx = "^0.24.1"
+httpx = ">=0.24.0,<1.0.0"
 aiohttp = "^3.8.5"
 aiohttp = "^3.8.5"
 
 
 [tool.poetry.extras]
 [tool.poetry.extras]
@@ -53,7 +53,7 @@ webdriver-manager = "^3.8.6"
 numpy = ">=1.24.0"
 numpy = ">=1.24.0"
 selenium = "^4.11.2"
 selenium = "^4.11.2"
 beautifulsoup4 = "^4.12.2"
 beautifulsoup4 = "^4.12.2"
-urllib3 = ">=1.26.17,^1.26 || >=2.0.6" # https://github.com/zauberzeug/nicegui/security/dependabot/22
+urllib3 = ">=1.26.18,^1.26 || >=2.0.7" # https://github.com/zauberzeug/nicegui/security/dependabot/23
 
 
 [build-system]
 [build-system]
 requires = [
 requires = [

+ 5 - 2
tests/test_context_menu.py

@@ -6,9 +6,12 @@ from .screen import Screen
 def test_context_menu(screen: Screen):
 def test_context_menu(screen: Screen):
     with ui.label('Right-click me'):
     with ui.label('Right-click me'):
         with ui.context_menu():
         with ui.context_menu():
-            ui.menu_item('Item 1')
+            ui.menu_item('Item 1', on_click=lambda: ui.label('Item 1 clicked'))
             ui.menu_item('Item 2')
             ui.menu_item('Item 2')
 
 
     screen.open('/')
     screen.open('/')
     screen.context_click('Right-click me')
     screen.context_click('Right-click me')
-    screen.should_contain('Item 1')
+    screen.click('Item 1')
+    screen.should_contain('Item 1 clicked')
+    screen.wait(0.5)
+    screen.should_not_contain('Item 2')

+ 23 - 0
tests/test_refreshable.py

@@ -180,3 +180,26 @@ def test_refresh_with_function_reference(screen: Screen):
     screen.should_contain('Refreshing A')
     screen.should_contain('Refreshing A')
     screen.click('B')
     screen.click('B')
     screen.should_contain('Refreshing B')
     screen.should_contain('Refreshing B')
+
+
+def test_refreshable_with_state(screen: Screen):
+    @ui.refreshable
+    def counter(title: str):
+        count, set_count = ui.state(0)
+        ui.label(f'{title}: {count}')
+        ui.button(f'Increment {title}', on_click=lambda: set_count(count + 1))
+
+    counter('A')
+    counter('B')
+
+    screen.open('/')
+    screen.should_contain('A: 0')
+    screen.should_contain('B: 0')
+
+    screen.click('Increment A')
+    screen.should_contain('A: 1')
+    screen.should_contain('B: 0')
+
+    screen.click('Increment B')
+    screen.should_contain('A: 1')
+    screen.should_contain('B: 1')

+ 5 - 5
tests/test_storage.py

@@ -91,7 +91,7 @@ async def test_access_user_storage_from_fastapi(screen: Screen):
         assert response.status_code == 200
         assert response.status_code == 200
         assert response.text == '"OK"'
         assert response.text == '"OK"'
         await asyncio.sleep(0.5)  # wait for storage to be written
         await asyncio.sleep(0.5)  # wait for storage to be written
-        assert next(Path('.nicegui').glob('storage_user_*.json')).read_text() == '{"msg": "yes"}'
+        assert next(Path('.nicegui').glob('storage_user_*.json')).read_text() == '{"msg":"yes"}'
 
 
 
 
 def test_access_user_storage_on_interaction(screen: Screen):
 def test_access_user_storage_on_interaction(screen: Screen):
@@ -105,7 +105,7 @@ def test_access_user_storage_on_interaction(screen: Screen):
     screen.open('/')
     screen.open('/')
     screen.click('switch')
     screen.click('switch')
     screen.wait(0.5)
     screen.wait(0.5)
-    assert '{"test_switch": true}' in next(Path('.nicegui').glob('storage_user_*.json')).read_text()
+    assert next(Path('.nicegui').glob('storage_user_*.json')).read_text() == '{"test_switch":true}'
 
 
 
 
 def test_access_user_storage_from_button_click_handler(screen: Screen):
 def test_access_user_storage_from_button_click_handler(screen: Screen):
@@ -117,7 +117,7 @@ def test_access_user_storage_from_button_click_handler(screen: Screen):
     screen.open('/')
     screen.open('/')
     screen.click('test')
     screen.click('test')
     screen.wait(1)
     screen.wait(1)
-    assert '{"inner_function": "works"}' in next(Path('.nicegui').glob('storage_user_*.json')).read_text()
+    assert next(Path('.nicegui').glob('storage_user_*.json')).read_text() == '{"inner_function":"works"}'
 
 
 
 
 async def test_access_user_storage_from_background_task(screen: Screen):
 async def test_access_user_storage_from_background_task(screen: Screen):
@@ -130,7 +130,7 @@ async def test_access_user_storage_from_background_task(screen: Screen):
 
 
     screen.ui_run_kwargs['storage_secret'] = 'just a test'
     screen.ui_run_kwargs['storage_secret'] = 'just a test'
     screen.open('/')
     screen.open('/')
-    assert '{"subtask": "works"}' in next(Path('.nicegui').glob('storage_user_*.json')).read_text()
+    assert next(Path('.nicegui').glob('storage_user_*.json')).read_text() == '{"subtask":"works"}'
 
 
 
 
 def test_user_and_general_storage_is_persisted(screen: Screen):
 def test_user_and_general_storage_is_persisted(screen: Screen):
@@ -166,4 +166,4 @@ def test_rapid_storage(screen: Screen):
     screen.open('/')
     screen.open('/')
     screen.click('test')
     screen.click('test')
     screen.wait(0.5)
     screen.wait(0.5)
-    assert '{"one": 1, "two": 2, "three": 3}' in Path('.nicegui', 'storage_general.json').read_text()
+    assert Path('.nicegui', 'storage_general.json').read_text() == '{"one":1,"two":2,"three":3}'

+ 1 - 1
website/documentation_tools.py

@@ -47,7 +47,7 @@ def subheading(text: str, *, make_menu_entry: bool = True, more_link: Optional[s
     if make_menu_entry:
     if make_menu_entry:
         with get_menu() as menu:
         with get_menu() as menu:
             async def click():
             async def click():
-                if await ui.run_javascript('!!document.querySelector("div.q-drawer__backdrop")'):
+                if await ui.run_javascript('!!document.querySelector("div.q-drawer__backdrop")', timeout=5.0):
                     menu.hide()
                     menu.hide()
                     ui.open(f'#{name}')
                     ui.open(f'#{name}')
             ui.link(text, target=f'#{name}').props('data-close-overlay').on('click', click, [])
             ui.link(text, target=f'#{name}').props('data-close-overlay').on('click', click, [])

+ 20 - 0
website/more_documentation/refreshable_documentation.py

@@ -67,3 +67,23 @@ def more() -> None:
                         ui.label(rule).classes('text-xs text-red')
                         ui.label(rule).classes('text-xs text-red')
 
 
         show_info()
         show_info()
+
+    @text_demo('Refreshable UI with reactive state', '''
+        You can create reactive state variables with the `ui.state` function, like `count` and `color` in this demo.
+        They can be used like normal variables for creating UI elements like the `ui.label`.
+        Their corresponding setter functions can be used to set new values, which will automatically refresh the UI.
+    ''')
+    def reactive_state():
+        @ui.refreshable
+        def counter(name: str):
+            with ui.card():
+                count, set_count = ui.state(0)
+                color, set_color = ui.state('black')
+                ui.label(f'{name} = {count}').classes(f'text-{color}')
+                ui.button(f'{name} += 1', on_click=lambda: set_count(count + 1))
+                ui.select(['black', 'red', 'green', 'blue'],
+                          value=color, on_change=lambda e: set_color(e.value))
+
+        with ui.row():
+            counter('A')
+            counter('B')

+ 1 - 1
website/search.py

@@ -52,7 +52,7 @@ class Search:
     def handle_input(self, e: events.ValueChangeEventArguments) -> None:
     def handle_input(self, e: events.ValueChangeEventArguments) -> None:
         async def handle_input():
         async def handle_input():
             with self.results:
             with self.results:
-                results = await ui.run_javascript(f'return window.fuse.search("{e.value}").slice(0, 50)')
+                results = await ui.run_javascript(f'return window.fuse.search("{e.value}").slice(0, 50)', timeout=6)
                 self.results.clear()
                 self.results.clear()
                 for result in results:
                 for result in results:
                     href: str = result['item']['url']
                     href: str = result['item']['url']