From 1424a8599efbc4d9e75e6f13e873b56bd630f9aa Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Thu, 19 Oct 2023 22:35:44 +0200 Subject: [PATCH] move client dict and index client into Client --- examples/opencv_webcam/main.py | 7 +++--- examples/ros2/ros2_ws/src/gui/gui/node.py | 6 ++--- nicegui/air.py | 17 +++++++------ nicegui/app.py | 11 ++++----- nicegui/client.py | 15 ++++++++++-- nicegui/elements/pyplot.py | 3 ++- nicegui/elements/timer.py | 3 ++- nicegui/functions/refreshable.py | 3 ++- nicegui/globals.py | 2 -- nicegui/nicegui.py | 18 +++++++------- nicegui/outbox.py | 30 +++++++++++------------ nicegui/storage.py | 4 +-- tests/conftest.py | 4 +-- 13 files changed, 66 insertions(+), 57 deletions(-) diff --git a/examples/opencv_webcam/main.py b/examples/opencv_webcam/main.py index a3c5baa23..a73704e50 100755 --- a/examples/opencv_webcam/main.py +++ b/examples/opencv_webcam/main.py @@ -7,8 +7,7 @@ import numpy as np from fastapi import Response -import nicegui.globals -from nicegui import app, run, ui +from nicegui import Client, app, run, ui # In case you don't have a webcam, this will provide a black placeholder image. black_1px = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYGBg+A8AAQQBAHAgZQsAAAAASUVORK5CYII=' @@ -46,8 +45,8 @@ async def grab_video_frame() -> Response: async def disconnect() -> None: """Disconnect all clients from current running server.""" - for client in nicegui.globals.clients.keys(): - await app.sio.disconnect(client) + for client_id in Client.instances: + await app.sio.disconnect(client_id) def handle_sigint(signum, frame) -> None: diff --git a/examples/ros2/ros2_ws/src/gui/gui/node.py b/examples/ros2/ros2_ws/src/gui/gui/node.py index cae1f9d27..58b426769 100644 --- a/examples/ros2/ros2_ws/src/gui/gui/node.py +++ b/examples/ros2/ros2_ws/src/gui/gui/node.py @@ -7,7 +7,7 @@ from rclpy.executors import ExternalShutdownException from rclpy.node import Node -from nicegui import app, globals, run, ui +from nicegui import Client, app, run, ui class NiceGuiNode(Node): @@ -17,7 +17,7 @@ def __init__(self) -> None: self.cmd_vel_publisher = self.create_publisher(Twist, 'cmd_vel', 1) self.subscription = self.create_subscription(Pose, 'pose', self.handle_pose, 1) - with globals.index_client: + with Client.index_client: with ui.row().classes('items-stretch'): with ui.card().classes('w-44 text-center items-center'): ui.label('Control').classes('text-2xl') @@ -67,7 +67,7 @@ def ros_main() -> None: rclpy.spin(node) except ExternalShutdownException: pass - + app.on_startup(lambda: threading.Thread(target=ros_main).start()) run.APP_IMPORT_STRING = f'{__name__}:app' # ROS2 uses a non-standard module name, so we need to specify it here diff --git a/nicegui/air.py b/nicegui/air.py index 3938dc3c1..8f6f89b22 100644 --- a/nicegui/air.py +++ b/nicegui/air.py @@ -8,6 +8,7 @@ from socketio import AsyncClient from . import background_tasks, globals # pylint: disable=redefined-builtin +from .client import Client from .logging import log from .nicegui import handle_disconnect, handle_event, handle_handshake, handle_javascript_response @@ -64,9 +65,9 @@ def _handleerror(data: Dict[str, Any]) -> None: @self.relay.on('handshake') def _handle_handshake(data: Dict[str, Any]) -> bool: client_id = data['client_id'] - if client_id not in globals.clients: + if client_id not in Client.instances: return False - client = globals.clients[client_id] + client = Client.instances[client_id] client.environ = data['environ'] client.on_air = True handle_handshake(client) @@ -75,17 +76,17 @@ def _handle_handshake(data: Dict[str, Any]) -> bool: @self.relay.on('client_disconnect') def _handle_disconnect(data: Dict[str, Any]) -> None: client_id = data['client_id'] - if client_id not in globals.clients: + if client_id not in Client.instances: return - client = globals.clients[client_id] + client = Client.instances[client_id] client.disconnect_task = background_tasks.create(handle_disconnect(client)) @self.relay.on('event') def _handle_event(data: Dict[str, Any]) -> None: client_id = data['client_id'] - if client_id not in globals.clients: + if client_id not in Client.instances: return - client = globals.clients[client_id] + client = Client.instances[client_id] if isinstance(data['msg']['args'], dict) and 'socket_id' in data['msg']['args']: data['msg']['args']['socket_id'] = client_id # HACK: translate socket_id of ui.scene's init event handle_event(client, data['msg']) @@ -93,9 +94,9 @@ def _handle_event(data: Dict[str, Any]) -> None: @self.relay.on('javascript_response') def _handle_javascript_response(data: Dict[str, Any]) -> None: client_id = data['client_id'] - if client_id not in globals.clients: + if client_id not in Client.instances: return - client = globals.clients[client_id] + client = Client.instances[client_id] handle_javascript_response(client, data['msg']) @self.relay.on('out_of_time') diff --git a/nicegui/app.py b/nicegui/app.py index 7b0060e31..28c00d820 100644 --- a/nicegui/app.py +++ b/nicegui/app.py @@ -8,6 +8,7 @@ from fastapi.staticfiles import StaticFiles from . import background_tasks, globals, helpers # pylint: disable=redefined-builtin +from .client import Client from .logging import log from .native import Native from .observables import ObservableSet @@ -59,17 +60,15 @@ def is_stopped(self) -> bool: def start(self) -> None: """Start NiceGUI. (For internal use only.)""" self._state = State.STARTING - with globals.index_client: - for t in self._startup_handlers: - helpers.safe_invoke(t) + for t in self._startup_handlers: + helpers.safe_invoke(t, Client.index_client) self._state = State.STARTED def stop(self) -> None: """Stop NiceGUI. (For internal use only.)""" self._state = State.STOPPING - with globals.index_client: - for t in self._shutdown_handlers: - helpers.safe_invoke(t) + for t in self._shutdown_handlers: + helpers.safe_invoke(t, Client.index_client) self._state = State.STOPPED def on_connect(self, handler: Union[Callable, Awaitable]) -> None: diff --git a/nicegui/client.py b/nicegui/client.py index 7380661cb..743683695 100644 --- a/nicegui/client.py +++ b/nicegui/client.py @@ -31,10 +31,16 @@ class Client: page_routes: Dict[Callable[..., Any], str] = {} """Maps page builders to their routes.""" + instances: Dict[str, Client] = {} + """Maps client IDs to clients.""" + + index_client: Client + """The client that is used to render the auto-index page.""" + def __init__(self, page: page, *, shared: bool = False) -> None: self.id = str(uuid.uuid4()) self.created = time.time() - globals.clients[self.id] = self + self.instances[self.id] = self self.elements: Dict[int, Element] = {} self.next_element_id: int = 0 @@ -62,6 +68,11 @@ def __init__(self, page: page, *, shared: bool = False) -> None: self._temporary_socket_id: Optional[str] = None + @property + def is_index_client(self) -> bool: + """Return True if this client is the auto-index client.""" + return self is self.index_client + @property def ip(self) -> Optional[str]: """Return the IP address of the client, or None if the client is not connected.""" @@ -128,7 +139,7 @@ async def disconnected(self, check_interval: float = 0.1) -> None: if not self.has_socket_connection: await self.connected() self.is_waiting_for_disconnect = True - while self.id in globals.clients: + while self.id in self.instances: await asyncio.sleep(check_interval) self.is_waiting_for_disconnect = False diff --git a/nicegui/elements/pyplot.py b/nicegui/elements/pyplot.py index 7430001e3..daafbc2a2 100644 --- a/nicegui/elements/pyplot.py +++ b/nicegui/elements/pyplot.py @@ -4,6 +4,7 @@ from typing import Any from .. import background_tasks, globals # pylint: disable=redefined-builtin +from ..client import Client from ..element import Element try: @@ -51,6 +52,6 @@ def __exit__(self, *_): self.update() async def _auto_close(self) -> None: - while self.client.id in globals.clients: + while self.client.id in Client.instances: await asyncio.sleep(1.0) plt.close(self.fig) diff --git a/nicegui/elements/timer.py b/nicegui/elements/timer.py index 0389a77d2..b7ee6af77 100644 --- a/nicegui/elements/timer.py +++ b/nicegui/elements/timer.py @@ -5,6 +5,7 @@ from .. import background_tasks, globals, helpers # pylint: disable=redefined-builtin from ..binding import BindableProperty +from ..client import Client from ..element import Element from ..logging import log @@ -115,7 +116,7 @@ async def _connected(self, timeout: float = 60.0) -> bool: def _should_stop(self) -> bool: return ( self.is_deleted or - self.client.id not in globals.clients or + self.client.id not in Client.instances or self._is_canceled or globals.app.is_stopping or globals.app.is_stopped diff --git a/nicegui/functions/refreshable.py b/nicegui/functions/refreshable.py index 3d441d714..e9583a831 100644 --- a/nicegui/functions/refreshable.py +++ b/nicegui/functions/refreshable.py @@ -6,6 +6,7 @@ from typing_extensions import Self from .. import background_tasks, globals # pylint: disable=redefined-builtin +from ..client import Client from ..dataclasses import KWONLY_SLOTS from ..element import Element from ..helpers import is_coroutine_function @@ -114,7 +115,7 @@ def prune(self) -> None: self.targets = [ target 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 Client.instances and target.container.id in target.container.client.elements ] diff --git a/nicegui/globals.py b/nicegui/globals.py index dd035bdb3..30d58cda0 100644 --- a/nicegui/globals.py +++ b/nicegui/globals.py @@ -39,8 +39,6 @@ socket_io_js_extra_headers: Dict = {} socket_io_js_transports: List[Literal['websocket', 'polling']] = ['websocket', 'polling'] # NOTE: we favor websocket slot_stacks: Dict[int, List[Slot]] = {} -clients: Dict[str, Client] = {} -index_client: Client quasar_config: Dict = { 'brand': { 'primary': '#5898d4', diff --git a/nicegui/nicegui.py b/nicegui/nicegui.py index ec5344b7d..4d70f2298 100644 --- a/nicegui/nicegui.py +++ b/nicegui/nicegui.py @@ -40,12 +40,12 @@ ) app.mount(f'/_nicegui/{__version__}/static', static_files, name='static') -globals.index_client = Client(page('/'), shared=True).__enter__() # pylint: disable=unnecessary-dunder-call +Client.index_client = Client(page('/'), shared=True).__enter__() # pylint: disable=unnecessary-dunder-call @app.get('/') def _get_index(request: Request) -> Response: - return globals.index_client.build_response(request) + return Client.index_client.build_response(request) @app.get(f'/_nicegui/{__version__}' + '/libraries/{key:path}') @@ -94,7 +94,7 @@ def handle_startup(with_welcome_message: bool = True) -> None: globals.loop = asyncio.get_running_loop() globals.app.start() background_tasks.create(binding.refresh_loop(), name='refresh bindings') - background_tasks.create(outbox.loop(), name='send outbox') + background_tasks.create(outbox.loop(Client.instances), name='send outbox') background_tasks.create(prune_clients(), name='prune clients') background_tasks.create(prune_slot_stacks(), name='prune slot stacks') if with_welcome_message: @@ -132,7 +132,7 @@ async def _exception_handler_500(request: Request, exception: Exception) -> Resp @sio.on('handshake') async def _on_handshake(sid: str, client_id: str) -> bool: - client = globals.clients.get(client_id) + client = Client.instances.get(client_id) if not client: return False client.environ = sio.get_environ(sid) @@ -157,7 +157,7 @@ def _on_disconnect(sid: str) -> None: query_bytes: bytearray = sio.get_environ(sid)['asgi.scope']['query_string'] query = urllib.parse.parse_qs(query_bytes.decode()) client_id = query['client_id'][0] - client = globals.clients.get(client_id) + client = Client.instances.get(client_id) if client: client.disconnect_task = background_tasks.create(handle_disconnect(client)) @@ -176,7 +176,7 @@ async def handle_disconnect(client: Client) -> None: @sio.on('event') def _on_event(_: str, msg: Dict) -> None: - client = globals.clients.get(msg['client_id']) + client = Client.instances.get(msg['client_id']) if not client or not client.has_socket_connection: return handle_event(client, msg) @@ -195,7 +195,7 @@ def handle_event(client: Client, msg: Dict) -> None: @sio.on('javascript_response') def _on_javascript_response(_: str, msg: Dict) -> None: - client = globals.clients.get(msg['client_id']) + client = Client.instances.get(msg['client_id']) if not client: return handle_javascript_response(client, msg) @@ -211,7 +211,7 @@ async def prune_clients() -> None: while True: stale_clients = [ id - for id, client in globals.clients.items() + for id, client in Client.instances.items() if not client.shared and not client.has_socket_connection and client.created < time.time() - 60.0 ] for client_id in stale_clients: @@ -238,4 +238,4 @@ async def prune_slot_stacks() -> None: def _delete_client(client_id: str) -> None: - globals.clients.pop(client_id).remove_all_elements() + Client.instances.pop(client_id).remove_all_elements() diff --git a/nicegui/outbox.py b/nicegui/outbox.py index 6cd9fb6ee..5eaf6ef48 100644 --- a/nicegui/outbox.py +++ b/nicegui/outbox.py @@ -7,6 +7,7 @@ from . import globals # pylint: disable=redefined-builtin if TYPE_CHECKING: + from .client import Client from .element import Element ClientId = str @@ -33,15 +34,19 @@ def enqueue_message(message_type: MessageType, data: Any, target_id: ClientId) - message_queue.append((target_id, message_type, data)) -async def _emit(message_type: MessageType, data: Any, target_id: ClientId) -> None: - await globals.sio.emit(message_type, data, room=target_id) - if _is_target_on_air(target_id): - assert globals.air is not None - await globals.air.emit(message_type, data, room=target_id) +async def loop(clients: Dict[str, Client]) -> None: + """Emit queued updates and messages in an endless loop.""" + def is_target_on_air(target_id: str) -> bool: + if target_id in clients: + return clients[target_id].on_air + return target_id in globals.sio.manager.rooms + async def emit(message_type: MessageType, data: Any, target_id: ClientId) -> None: + await globals.sio.emit(message_type, data, room=target_id) + if is_target_on_air(target_id): + assert globals.air is not None + await globals.air.emit(message_type, data, room=target_id) -async def loop() -> None: - """Emit queued updates and messages in an endless loop.""" while True: if not update_queue and not message_queue: await asyncio.sleep(0.01) @@ -54,11 +59,11 @@ async def loop() -> None: element_id: None if element is None else element._to_dict() # pylint: disable=protected-access for element_id, element in elements.items() } - coros.append(_emit('update', data, client_id)) + coros.append(emit('update', data, client_id)) update_queue.clear() for target_id, message_type, data in message_queue: - coros.append(_emit(message_type, data, target_id)) + coros.append(emit(message_type, data, target_id)) message_queue.clear() for coro in coros: @@ -69,10 +74,3 @@ async def loop() -> None: except Exception as e: globals.app.handle_exception(e) await asyncio.sleep(0.1) - - -def _is_target_on_air(target_id: str) -> bool: - if target_id in globals.clients: - return globals.clients[target_id].on_air - - return target_id in globals.sio.manager.rooms diff --git a/nicegui/storage.py b/nicegui/storage.py index 11885030a..8004a3b0b 100644 --- a/nicegui/storage.py +++ b/nicegui/storage.py @@ -99,7 +99,7 @@ def browser(self) -> Union[ReadOnlyDict, Dict]: """ request: Optional[Request] = request_contextvar.get() if request is None: - if globals.get_client() == globals.index_client: + if globals.get_client().is_index_client: raise RuntimeError('app.storage.browser can only be used with page builder functions ' '(https://nicegui.io/documentation/page)') raise RuntimeError('app.storage.browser needs a storage_secret passed in ui.run()') @@ -119,7 +119,7 @@ def user(self) -> Dict: """ request: Optional[Request] = request_contextvar.get() if request is None: - if globals.get_client() == globals.index_client: + if globals.get_client().is_index_client: raise RuntimeError('app.storage.user can only be used with page builder functions ' '(https://nicegui.io/documentation/page)') raise RuntimeError('app.storage.user needs a storage_secret passed in ui.run()') diff --git a/tests/conftest.py b/tests/conftest.py index f7b5a05a2..6b635550e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -59,8 +59,8 @@ def reset_globals() -> Generator[None, None, None]: importlib.reload(plotly) importlib.reload(pyplot) globals.app.storage.clear() - globals.index_client = Client(page('/'), shared=True).__enter__() - globals.app.get('/')(globals.index_client.build_response) + Client.index_client = Client(page('/'), shared=True).__enter__() + globals.app.get('/')(Client.index_client.build_response) binding.reset()