Skip to content

Commit

Permalink
move client dict and index client into Client
Browse files Browse the repository at this point in the history
  • Loading branch information
falkoschindler committed Oct 19, 2023
1 parent 84a9a41 commit 1424a85
Show file tree
Hide file tree
Showing 13 changed files with 66 additions and 57 deletions.
7 changes: 3 additions & 4 deletions examples/opencv_webcam/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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='
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions examples/ros2/ros2_ws/src/gui/gui/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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')
Expand Down Expand Up @@ -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
Expand Down
17 changes: 9 additions & 8 deletions nicegui/air.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -75,27 +76,27 @@ 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'])

@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')
Expand Down
11 changes: 5 additions & 6 deletions nicegui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
15 changes: 13 additions & 2 deletions nicegui/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion nicegui/elements/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
3 changes: 2 additions & 1 deletion nicegui/elements/timer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion nicegui/functions/refreshable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
]


Expand Down
2 changes: 0 additions & 2 deletions nicegui/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
18 changes: 9 additions & 9 deletions nicegui/nicegui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}')
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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))

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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()
30 changes: 14 additions & 16 deletions nicegui/outbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from . import globals # pylint: disable=redefined-builtin

if TYPE_CHECKING:
from .client import Client
from .element import Element

ClientId = str
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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
4 changes: 2 additions & 2 deletions nicegui/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()')
Expand All @@ -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()')
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down

0 comments on commit 1424a85

Please sign in to comment.