Procházet zdrojové kódy

Merge branch 'zauberzeug:main' into main

Artem Revenko před 1 rokem
rodič
revize
9a2cf9d147

+ 3 - 3
fly.toml

@@ -38,8 +38,8 @@ kill_timeout = "5s"
     handlers = ["tls", "http"]
   [services.concurrency]
     type = "requests"
-    hard_limit = 60
-    soft_limit = 30
+    hard_limit = 50
+    soft_limit = 20
 
   [[services.tcp_checks]]
     interval = "10s"
@@ -56,5 +56,5 @@ kill_timeout = "5s"
     tls_skip_verify = false
 
 [[metrics]]
+  path = "/"
   port = 9062
-  path = "/metrics"

+ 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, [])

+ 36 - 22
nicegui/nicegui.py

@@ -171,7 +171,7 @@ async def handle_disconnect(client: Client) -> None:
     delay = client.page.reconnect_timeout if client.page.reconnect_timeout is not None else globals.reconnect_timeout
     await asyncio.sleep(delay)
     if not client.shared:
-        delete_client(client.id)
+        delete_client(client)
     for t in client.disconnect_handlers:
         safe_invoke(t, client)
     for t in globals.disconnect_handlers:
@@ -210,32 +210,46 @@ def handle_javascript_response(client: Client, msg: Dict) -> None:
 
 async def prune_clients() -> None:
     while True:
-        stale_clients = [
-            id
-            for id, client in globals.clients.items()
-            if not client.shared and not client.has_socket_connection and client.created < time.time() - 60.0
-        ]
-        for client_id in stale_clients:
-            delete_client(client_id)
+        try:
+            stale_clients = [
+                client
+                for client in globals.clients.values()
+                if not client.shared and not client.has_socket_connection and client.created < time.time() - 60.0
+            ]
+            for client in stale_clients:
+                delete_client(client)
+        except Exception:
+            # NOTE: make sure the loop doesn't crash
+            globals.log.exception('Error while pruning clients')
         await asyncio.sleep(10)
 
 
 async def prune_slot_stacks() -> None:
     while True:
-        running = [
-            id(task)
-            for task in asyncio.tasks.all_tasks()
-            if not task.done() and not task.cancelled()
-        ]
-        stale = [
-            id_
-            for id_ in globals.slot_stacks
-            if id_ not in running
-        ]
-        for id_ in stale:
-            del globals.slot_stacks[id_]
+        try:
+            running = [
+                id(task)
+                for task in asyncio.tasks.all_tasks()
+                if not task.done() and not task.cancelled()
+            ]
+            stale = [
+                id_
+                for id_ in globals.slot_stacks
+                if id_ not in running
+            ]
+            for id_ in stale:
+                del globals.slot_stacks[id_]
+        except Exception:
+            # NOTE: make sure the loop doesn't crash
+            globals.log.exception('Error while pruning slot stacks')
         await asyncio.sleep(10)
 
 
-def delete_client(client_id: str) -> None:
-    globals.clients.pop(client_id).remove_all_elements()
+def delete_client(client: Client) -> None:
+    """Delete a client and all its elements.
+
+    If the global clients dictionary does not contain the client, its elements are still removed and a KeyError is raised.
+    Normally this should never happen, but has been observed (see #1826).
+    """
+    client.remove_all_elements()
+    del globals.clients[client.id]

+ 1 - 1
poetry.lock

@@ -3051,4 +3051,4 @@ plotly = ["plotly"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "6f9bfd00c2f9403878bfec9b329b0d56b774fbb10f81091723e73ecb2288b666"
+content-hash = "c333a9e892ce9161dae4d7e4109bd33cfa9ec8798802bb76231dcbf141beb2f3"

+ 1 - 1
pyproject.toml

@@ -16,7 +16,7 @@ Pygments = ">=2.15.1,<3.0.0"
 uvicorn = {extras = ["standard"], version = "^0.22.0"}
 fastapi = ">=0.92,<1.0.0"
 fastapi-socketio = "^0.0.10"
-python-socketio = ">=5.9.0" # https://github.com/zauberzeug/nicegui/issues/1809
+python-socketio = ">=5.10.0" # https://github.com/zauberzeug/nicegui/issues/1809
 vbuild = ">=0.8.2"
 watchfiles = ">=0.18.1,<1.0.0"
 jinja2 = "^3.1.2"

+ 2 - 2
set_scale.sh

@@ -1,10 +1,10 @@
 #!/usr/bin/env bash
 
-fly scale count app=3  --region fra -y
+fly scale count app=4  --region fra -y
 fly scale count app=3  --region iad -y
 fly scale count app=1  --region jnb -y
 fly scale count app=3  --region lax -y
-fly scale count app=3  --region lhr -y
+fly scale count app=4  --region lhr -y
 fly scale count app=2  --region bom -y
 fly scale count app=2  --region mad -y
 fly scale count app=3  --region mia -y

+ 2 - 1
tests/conftest.py

@@ -89,7 +89,8 @@ def screen(driver: webdriver.Chrome, request: pytest.FixtureRequest, caplog: pyt
     if screen_.is_open:
         screen_.shot(request.node.name)
     logs = screen_.caplog.get_records('call')
-    assert not logs, f'There were unexpected logs:\n-------\n{logs}\n-------'
     screen_.stop_server()
     if DOWNLOAD_DIR.exists():
         shutil.rmtree(DOWNLOAD_DIR)
+    if logs:
+        pytest.fail('There were unexpected logs. See "Captured log call" below.', pytrace=False)

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

+ 1 - 1
website/static/search_index.json

@@ -1271,7 +1271,7 @@
   },
   {
     "title": "Context Menu",
-    "content": "Creates a context menu based on Quasar's QMenu <https://quasar.dev/vue-components/menu>_ component. The context menu should be placed inside the element where it should be shown. It is automatically opened when the user right-clicks on the element and appears at the mouse position.",
+    "content": "Creates a context menu based on Quasar's QMenu <https://quasar.dev/vue-components/menu>_ component. The context menu should be placed inside the element where it should be shown. It is automatically opened when the user right-clicks on the element and appears at the mouse position. open Open the context menu.close Close the context menu.",
     "url": "/documentation/context_menu"
   },
   {