瀏覽代碼

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
 
-RUN apt update && apt install curl -y
+RUN apt update && apt install curl build-essential -y
 
 # We use Poetry for dependency management
 RUN curl -sSL https://install.python-poetry.org | python3 - && \

+ 3 - 3
fly.toml

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

+ 8 - 0
nicegui/elements/context_menu.py

@@ -13,3 +13,11 @@ class ContextMenu(Element):
         super().__init__('q-menu')
         self._props['context-menu'] = 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 ..events import ClickEventArguments, handle_event
+from .context_menu import ContextMenu
 from .mixins.text_element import TextElement
 from .mixins.value_element import ValueElement
 
@@ -65,6 +66,6 @@ class MenuItem(TextElement):
         def handle_click(_) -> None:
             handle_event(on_click, ClickEventArguments(sender=self, client=self.client))
             if auto_close:
-                assert isinstance(self.menu, Menu)
+                assert isinstance(self.menu, (Menu, ContextMenu))
                 self.menu.close()
         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 ..binding import BindableProperty
-from ..slot import Slot
+from ..element import Element
 
 
-class Timer:
+class Timer(Element, component='timer.js'):
     active = 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 once: whether the callback is only executed once after a delay specified by `interval` (default: `False`)
         """
+        super().__init__()
         self.interval = interval
         self.callback: Optional[Callable[..., Any]] = callback
         self.active = active
-        self.slot: Optional[Slot] = globals.get_slot()
         self._is_canceled: bool = False
 
         coroutine = self._run_once if once else self._run_in_loop
@@ -57,8 +57,7 @@ class Timer:
         try:
             if not await self._connected():
                 return
-            assert self.slot is not None
-            with self.slot:
+            with self.parent_slot:
                 await asyncio.sleep(self.interval)
                 if self.active and not self._should_stop():
                     await self._invoke_callback()
@@ -69,8 +68,7 @@ class Timer:
         try:
             if not await self._connected():
                 return
-            assert self.slot is not None
-            with self.slot:
+            with self.parent_slot:
                 while not self._should_stop():
                     try:
                         start = time.time()
@@ -101,27 +99,24 @@ class Timer:
         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.
         """
-        assert self.slot is not None
-        if self.slot.parent.client.shared:
+        if self.client.shared:
             return True
 
         # ignore served pages which do not reconnect to backend (e.g. monitoring requests, scrapers etc.)
         try:
-            await self.slot.parent.client.connected(timeout=timeout)
+            await self.client.connected(timeout=timeout)
             return True
         except TimeoutError:
             globals.log.error(f'Timer cancelled because client is not connected after {timeout} seconds')
             return False
 
     def _should_stop(self) -> bool:
-        assert self.slot is not None
         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
             globals.state in {globals.State.STOPPING, globals.State.STOPPED}
         )
 
     def _cleanup(self) -> None:
-        self.slot = 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
 
@@ -11,13 +13,20 @@ from ..helpers import is_coroutine_function
 
 @dataclass(**KWONLY_SLOTS)
 class RefreshableTarget:
-    container: Element
+    container: RefreshableContainer
+    refreshable: refreshable
     instance: Any
     args: Tuple[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]:
         """Run the function and return the result."""
+        RefreshableTarget.current_target = self
+        self.next_index = 0
         # pylint: disable=no-else-return
         if is_coroutine_function(func):
             async def wait_for_result() -> None:
@@ -67,7 +76,8 @@ class refreshable:
 
     def __call__(self, *args: Any, **kwargs: Any) -> Union[None, Awaitable]:
         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)
         return target.run(self.func)
 
@@ -106,3 +116,21 @@ class refreshable:
             for target in self.targets
             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 json
 import uuid
 from collections.abc import MutableMapping
 from pathlib import Path
@@ -12,7 +11,7 @@ from starlette.middleware.sessions import SessionMiddleware
 from starlette.requests import Request
 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)
 

+ 4 - 3
nicegui/ui.py

@@ -70,6 +70,7 @@ __all__ = [
     'tabs',
     'textarea',
     'time',
+    'timer',
     'timeline',
     'timeline_entry',
     'toggle',
@@ -84,7 +85,7 @@ __all__ = [
     'notify',
     'open',
     'refreshable',
-    'timer',
+    'state',
     'update',
     'page',
     'drawer',
@@ -170,6 +171,7 @@ from .elements.textarea import Textarea as textarea
 from .elements.time import Time as time
 from .elements.timeline import Timeline as timeline
 from .elements.timeline import TimelineEntry as timeline_entry
+from .elements.timer import Timer as timer
 from .elements.toggle import Toggle as toggle
 from .elements.tooltip import Tooltip as tooltip
 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.notify import notify
 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 .page import page
 from .page_layout import Drawer as drawer

+ 55 - 54
poetry.lock

@@ -878,13 +878,13 @@ files = [
 
 [[package]]
 name = "httpcore"
-version = "0.17.3"
+version = "0.18.0"
 description = "A minimal low-level HTTP client."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 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]
@@ -899,46 +899,47 @@ socks = ["socksio (==1.*)"]
 
 [[package]]
 name = "httptools"
-version = "0.6.0"
+version = "0.6.1"
 description = "A collection of framework independent HTTP protocol utils."
 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]
@@ -946,18 +947,18 @@ test = ["Cython (>=0.29.24,<0.30.0)"]
 
 [[package]]
 name = "httpx"
-version = "0.24.1"
+version = "0.25.0"
 description = "The next generation HTTP client."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 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]
 certifi = "*"
-httpcore = ">=0.15.0,<0.18.0"
+httpcore = ">=0.18.0,<0.19.0"
 idna = "*"
 sniffio = "*"
 
@@ -1546,13 +1547,13 @@ files = [
 
 [[package]]
 name = "outcome"
-version = "1.2.0"
+version = "1.3.0"
 description = "Capture the outcome of Python function calls."
 optional = false
 python-versions = ">=3.7"
 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]
@@ -2637,13 +2638,13 @@ files = [
 
 [[package]]
 name = "urllib3"
-version = "2.0.6"
+version = "2.0.7"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
 optional = false
 python-versions = ">=3.7"
 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]
@@ -3050,4 +3051,4 @@ plotly = ["plotly"]
 [metadata]
 lock-version = "2.0"
 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 }
 matplotlib = { version = "^3.5.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"
 
 [tool.poetry.extras]
@@ -53,7 +53,7 @@ webdriver-manager = "^3.8.6"
 numpy = ">=1.24.0"
 selenium = "^4.11.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]
 requires = [

+ 5 - 2
tests/test_context_menu.py

@@ -6,9 +6,12 @@ from .screen import Screen
 def test_context_menu(screen: Screen):
     with ui.label('Right-click me'):
         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')
 
     screen.open('/')
     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.click('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.text == '"OK"'
         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):
@@ -105,7 +105,7 @@ def test_access_user_storage_on_interaction(screen: Screen):
     screen.open('/')
     screen.click('switch')
     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):
@@ -117,7 +117,7 @@ def test_access_user_storage_from_button_click_handler(screen: Screen):
     screen.open('/')
     screen.click('test')
     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):
@@ -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.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):
@@ -166,4 +166,4 @@ def test_rapid_storage(screen: Screen):
     screen.open('/')
     screen.click('test')
     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:
         with get_menu() as menu:
             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()
                     ui.open(f'#{name}')
             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')
 
         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:
         async def handle_input():
             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()
                 for result in results:
                     href: str = result['item']['url']