0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31:50 +02:00

feat: add rpc broadcast

This commit is contained in:
2025-03-13 10:06:34 +01:00
committed by wyzula-jan
parent 6282210698
commit 1c322cc20a
14 changed files with 780 additions and 635 deletions

View File

@ -9,17 +9,20 @@ import os
import select import select
import subprocess import subprocess
import threading import threading
import time
from contextlib import contextmanager from contextlib import contextmanager
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Any
from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from bec_lib.utils.import_utils import lazy_import, lazy_import_from from bec_lib.utils.import_utils import lazy_import, lazy_import_from
from rich.console import Console from rich.console import Console
from rich.table import Table from rich.table import Table
import bec_widgets.cli.client as client import bec_widgets.cli.client as client
from bec_widgets.cli.auto_updates import AutoUpdates
from bec_widgets.cli.rpc.rpc_base import RPCBase # from bec_widgets.cli.auto_updates import AutoUpdates
from bec_widgets.cli.rpc.rpc_base import RPCBase, RPCReference
if TYPE_CHECKING: if TYPE_CHECKING:
from bec_lib import messages from bec_lib import messages
@ -166,7 +169,7 @@ class WidgetNameSpace:
docs = docs if docs else "No description available" docs = docs if docs else "No description available"
table.add_row(attr, docs) table.add_row(attr, docs)
console.print(table) console.print(table)
return f"" return ""
class AvailableWidgetsNamespace: class AvailableWidgetsNamespace:
@ -189,22 +192,13 @@ class AvailableWidgetsNamespace:
docs = docs if docs else "No description available" docs = docs if docs else "No description available"
table.add_row(attr_name, docs if len(docs.strip()) > 0 else "No description available") table.add_row(attr_name, docs if len(docs.strip()) > 0 else "No description available")
console.print(table) console.print(table)
return "" # f"<{self.__class__.__name__}>" return ""
class BECDockArea(client.BECDockArea):
"""Extend the BECDockArea class and add namespaces to access widgets of docks."""
def __init__(self, gui_id=None, config=None, name=None, parent=None):
super().__init__(gui_id, config, name, parent)
# Add namespaces for DockArea
self.elements = WidgetNameSpace()
class BECGuiClient(RPCBase): class BECGuiClient(RPCBase):
"""BEC GUI client class. Container for GUI applications within Python.""" """BEC GUI client class. Container for GUI applications within Python."""
_top_level: dict[str, BECDockArea] = {} _top_level: dict[str, client.BECDockArea] = {}
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
@ -217,6 +211,32 @@ class BECGuiClient(RPCBase):
self._gui_started_event = threading.Event() self._gui_started_event = threading.Event()
self._process = None self._process = None
self._process_output_processing_thread = None self._process_output_processing_thread = None
self._exposed_dock_areas = []
self._registry_state = {}
self._ipython_registry = {}
self.available_widgets = AvailableWidgetsNamespace()
####################
#### Client API ####
####################
def connect_to_gui_server(self, gui_id: str) -> None:
"""Connect to a GUI server"""
# Unregister the old callback
self._client.connector.unregister(
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
)
self._gui_id = gui_id
# Get the registry state
msgs = self._client.connector.xread(
MessageEndpoints.gui_registry_state(self._gui_id), count=1
)
if msgs:
self._handle_registry_update(msgs[0])
# Register the new callback
self._client.connector.register(
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
)
@property @property
def windows(self) -> dict: def windows(self) -> dict:
@ -228,6 +248,282 @@ class BECGuiClient(RPCBase):
"""List with dock areas in the GUI.""" """List with dock areas in the GUI."""
return list(self._top_level.values()) return list(self._top_level.values())
def start(self, wait: bool = False) -> None:
"""Start the GUI server."""
return self._start(wait=wait)
def show(self):
"""Show the GUI window."""
if self._process is not None:
return self._show_all()
# backward compatibility: show() was also starting server
return self._start_server(wait=True)
def hide(self):
"""Hide the GUI window."""
return self._hide_all()
def new(
self,
name: str | None = None,
wait: bool = True,
geometry: tuple[int, int, int, int] | None = None,
) -> client.BECDockArea:
"""Create a new top-level dock area.
Args:
name(str, optional): The name of the dock area. Defaults to None.
wait(bool, optional): Whether to wait for the server to start. Defaults to True.
geometry(tuple[int, int, int, int] | None): The geometry of the dock area (pos_x, pos_y, w, h)
Returns:
client.BECDockArea: The new dock area.
"""
if len(self.window_list) == 0:
self.show()
if wait:
with wait_for_server(self):
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
widget = rpc_client._run_rpc(
"new_dock_area", name, geometry
) # pylint: disable=protected-access
self._top_level[widget.widget_name] = widget
return widget
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
widget = rpc_client._run_rpc(
"new_dock_area", name, geometry
) # pylint: disable=protected-access
self._top_level[widget.widget_name] = widget
return widget
def delete(self, name: str) -> None:
"""Delete a dock area.
Args:
name(str): The name of the dock area.
"""
widget = self.windows.get(name)
if widget is None:
raise ValueError(f"Dock area {name} not found.")
widget._run_rpc("close") # pylint: disable=protected-access
def delete_all(self) -> None:
"""Delete all dock areas."""
for widget_name in self.windows:
self.delete(widget_name)
def kill_server(self) -> None:
"""Kill the GUI server."""
self._top_level.clear()
self._killed = True
if self._gui_started_timer is not None:
self._gui_started_timer.cancel()
self._gui_started_timer.join()
if self._process is None:
return
if self._process:
logger.success("Stopping GUI...")
self._process.terminate()
if self._process_output_processing_thread:
self._process_output_processing_thread.join()
self._process.wait()
self._process = None
# Unregister the registry state
self._client.connector.unregister(
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
)
def close(self):
"""Deprecated. Use kill_server() instead."""
# FIXME, deprecated in favor of kill, will be removed in the future
self.kill_server()
#########################
#### Private methods ####
#########################
def _gui_post_startup(self):
timeout = 10
while time.time() < time.time() + timeout:
if len(list(self._registry_state.keys())) == 0:
time.sleep(0.1)
else:
break
# FIXME AUTO UPDATES
# if self._auto_updates_enabled:
# if self._auto_updates is None:
# auto_updates = self._get_update_script()
# if auto_updates is None:
# AutoUpdates.create_default_dock = True
# AutoUpdates.enabled = True
# auto_updates = AutoUpdates(self._top_level["main"].widget)
# if auto_updates.create_default_dock:
# auto_updates.start_default_dock()
# self._start_update_script()
# self._auto_updates = auto_updates
self._do_show_all()
self._gui_started_event.set()
def _start_server(self, wait: bool = False) -> None:
"""
Start the GUI server, and execute callback when it is launched
"""
if self._process is None or self._process.poll() is not None:
logger.success("GUI starting...")
self._startup_timeout = 5
self._gui_started_event.clear()
self._process, self._process_output_processing_thread = _start_plot_process(
self._gui_id,
self.__class__,
gui_class_id=self._default_dock_name,
config=self._client._service_config.config, # pylint: disable=protected-access
logger=logger,
)
def gui_started_callback(callback):
try:
if callable(callback):
callback()
finally:
threading.current_thread().cancel()
self._gui_started_timer = RepeatTimer(
0.5, lambda: self._gui_is_alive() and gui_started_callback(self._gui_post_startup)
)
self._gui_started_timer.start()
if wait:
self._gui_started_event.wait()
def _dump(self):
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
return rpc_client._run_rpc("_dump")
def _start(self, wait: bool = False) -> None:
self._killed = False
self._client.connector.register(
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
)
return self._start_server(wait=wait)
def _handle_registry_update(self, msg: StreamMessage) -> None:
self._registry_state = msg["data"].state
self._update_dynamic_namespace()
def _do_show_all(self):
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
rpc_client._run_rpc("show") # pylint: disable=protected-access
for window in self._top_level.values():
window.show()
def _show_all(self):
with wait_for_server(self):
return self._do_show_all()
def _hide_all(self):
with wait_for_server(self):
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
rpc_client._run_rpc("hide") # pylint: disable=protected-access
# because of the registry callbacks, we may have
# dock areas that are already killed, but not yet
# removed from the registry state
if not self._killed:
for window in self._top_level.values():
window.hide()
def _update_dynamic_namespace(self):
"""Update the dynamic name space"""
self._clear_exposed_dock_areas()
self._cleanup_ipython_registry()
self._add_registry_to_namespace()
def _clear_exposed_dock_areas(self):
"""Clear the exposed dock areas"""
self._top_level.clear()
for widget_id in self._exposed_dock_areas:
delattr(self, widget_id)
self._exposed_dock_areas.clear()
def _cleanup_ipython_registry(self):
"""Cleanup the ipython registry"""
remove_ids = []
for widget_id in self._ipython_registry:
if widget_id not in self._registry_state:
remove_ids.append(widget_id)
for widget_id in remove_ids:
self._ipython_registry.pop(widget_id)
def _set_dynamic_attributes(self, obj: object, name: str, value: Any) -> None:
"""Add an object to the namespace"""
setattr(obj, name, value)
def _add_registry_to_namespace(self) -> None:
"""Add registry to namespace"""
# Add dock areas
dock_area_states = [
state
for state in self._registry_state.values()
if state["widget_class"] == "BECDockArea"
]
for state in dock_area_states:
# obj is an RPC reference to the RPCBase object
dock_area_obj = self._add_widget(state, self)
self._set_dynamic_attributes(self, dock_area_obj.widget_name, dock_area_obj)
# Add dock_area to the top level
self._top_level[dock_area_obj.widget_name] = dock_area_obj
self._exposed_dock_areas.append(dock_area_obj.widget_name)
# Add docks
dock_states = [
state
for state in self._registry_state.values()
if state["config"].get("parent_id", "") == dock_area_obj._gui_id
]
for state in dock_states:
dock_obj = self._add_widget(state, dock_area_obj)
self._set_dynamic_attributes(dock_area_obj, dock_obj.widget_name, dock_obj)
# Add widgets
widget_states = [
state
for state in self._registry_state.values()
if state["config"].get("parent_id", "") == dock_obj._gui_id
]
for state in widget_states:
widget = self._add_widget(state, dock_obj)
self._set_dynamic_attributes(dock_obj, widget.widget_name, widget)
self._set_dynamic_attributes(dock_area_obj.elements, widget.widget_name, widget)
def _add_widget(self, state: dict, parent: object) -> RPCReference:
"""Add a widget to the namespace
Args:
state (dict): The state of the widget from the _registry_state.
parent (object): The parent object.
"""
name = state["name"]
gui_id = state["gui_id"]
widget_class = getattr(client, state["widget_class"])
obj = self._ipython_registry.get(gui_id)
if obj is None:
widget = widget_class(gui_id=gui_id, name=name, parent=parent)
self._ipython_registry[gui_id] = widget
else:
widget = obj
if widget_class == client.BECDockArea:
# Add elements to dynamic namespace
self._set_dynamic_attributes(widget, "elements", WidgetNameSpace())
obj = RPCReference(registry=self._ipython_registry, gui_id=gui_id)
return obj
################################
#### Auto updates ####
#### potentially deprecated ####
################################
# FIXME AUTO UPDATES # FIXME AUTO UPDATES
# @property # @property
# def auto_updates(self): # def auto_updates(self):
@ -235,19 +531,19 @@ class BECGuiClient(RPCBase):
# with wait_for_server(self): # with wait_for_server(self):
# return self._auto_updates # return self._auto_updates
def _get_update_script(self) -> AutoUpdates | None: # def _get_update_script(self) -> AutoUpdates | None:
eps = imd.entry_points(group="bec.widgets.auto_updates") # eps = imd.entry_points(group="bec.widgets.auto_updates")
for ep in eps: # for ep in eps:
if ep.name == "plugin_widgets_update": # if ep.name == "plugin_widgets_update":
try: # try:
spec = importlib.util.find_spec(ep.module) # spec = importlib.util.find_spec(ep.module)
# if the module is not found, we skip it # # if the module is not found, we skip it
if spec is None: # if spec is None:
continue # continue
return ep.load()(gui=self._top_level["main"]) # return ep.load()(gui=self._top_level["main"])
except Exception as e: # except Exception as e:
logger.error(f"Error loading auto update script from plugin: {str(e)}") # logger.error(f"Error loading auto update script from plugin: {str(e)}")
return None # return None
# FIXME AUTO UPDATES # FIXME AUTO UPDATES
# @property # @property
@ -292,176 +588,14 @@ class BECGuiClient(RPCBase):
# if self._auto_updates_enabled: # if self._auto_updates_enabled:
# return self.auto_updates.do_update(msg) # return self.auto_updates.do_update(msg)
def _gui_post_startup(self):
# if self._auto_updates_enabled:
# if self._auto_updates is None:
# auto_updates = self._get_update_script()
# if auto_updates is None:
# AutoUpdates.create_default_dock = True
# AutoUpdates.enabled = True
# auto_updates = AutoUpdates(self._top_level["main"].widget)
# if auto_updates.create_default_dock:
# auto_updates.start_default_dock()
# self._start_update_script()
# self._auto_updates = auto_updates
self._top_level[self._default_dock_name] = BECDockArea(
gui_id=f"{self._default_dock_name}", name=self._default_dock_name, parent=self
)
self._do_show_all()
self._gui_started_event.set()
def _start_server(self, wait: bool = False) -> None:
"""
Start the GUI server, and execute callback when it is launched
"""
if self._process is None or self._process.poll() is not None:
logger.success("GUI starting...")
self._startup_timeout = 5
self._gui_started_event.clear()
self._process, self._process_output_processing_thread = _start_plot_process(
self._gui_id,
self.__class__,
gui_class_id=self._default_dock_name,
config=self._client._service_config.config, # pylint: disable=protected-access
logger=logger,
)
def gui_started_callback(callback):
try:
if callable(callback):
callback()
finally:
threading.current_thread().cancel()
self._gui_started_timer = RepeatTimer(
0.5, lambda: self._gui_is_alive() and gui_started_callback(self._gui_post_startup)
)
self._gui_started_timer.start()
if wait:
self._gui_started_event.wait()
def _dump(self):
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
return rpc_client._run_rpc("_dump")
def start(self, wait: bool = True) -> None:
"""Start the server and show the GUI window."""
return self._start_server(wait=wait)
def _do_show_all(self):
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
rpc_client._run_rpc("show") # pylint: disable=protected-access
for window in self._top_level.values():
window.show()
def _show_all(self):
with wait_for_server(self):
return self._do_show_all()
def _hide_all(self):
with wait_for_server(self):
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
rpc_client._run_rpc("hide") # pylint: disable=protected-access
# because of the registry callbacks, we may have
# dock areas that are already killed, but not yet
# removed from the registry state
if not self._killed:
for window in self._top_level.values():
window.hide()
def show(self):
"""Show the GUI window."""
if self._process is not None:
return self._show_all()
# backward compatibility: show() was also starting server
return self._start_server(wait=True)
def hide(self):
"""Hide the GUI window."""
return self._hide_all()
def new(
self,
name: str | None = None,
wait: bool = True,
geometry: tuple[int, int, int, int] | None = None,
) -> BECDockArea:
"""Create a new top-level dock area.
Args:
name(str, optional): The name of the dock area. Defaults to None.
wait(bool, optional): Whether to wait for the server to start. Defaults to True.
geometry(tuple[int, int, int, int] | None): The geometry of the dock area (pos_x, pos_y, w, h)
Returns:
BECDockArea: The new dock area.
"""
if len(self.window_list) == 0:
self.show()
if wait:
with wait_for_server(self):
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
widget = rpc_client._run_rpc(
"new_dock_area", name, geometry
) # pylint: disable=protected-access
self._top_level[widget.widget_name] = widget
return widget
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
widget = rpc_client._run_rpc(
"new_dock_area", name, geometry
) # pylint: disable=protected-access
self._top_level[widget.widget_name] = widget
return widget
def delete(self, name: str) -> None:
"""Delete a dock area.
Args:
name(str): The name of the dock area.
"""
widget = self.windows.get(name)
if widget is None:
raise ValueError(f"Dock area {name} not found.")
widget._run_rpc("close") # pylint: disable=protected-access
def delete_all(self) -> None:
"""Delete all dock areas."""
for widget_name in self.windows.keys():
self.delete(widget_name)
def close(self):
"""Deprecated. Use kill_server() instead."""
# FIXME, deprecated in favor of kill, will be removed in the future
self.kill_server()
def kill_server(self) -> None:
"""Kill the GUI server."""
self._top_level.clear()
self._killed = True
if self._gui_started_timer is not None:
self._gui_started_timer.cancel()
self._gui_started_timer.join()
if self._process is None:
return
if self._process:
logger.success("Stopping GUI...")
self._process.terminate()
if self._process_output_processing_thread:
self._process_output_processing_thread.join()
self._process.wait()
self._process = None
if __name__ == "__main__": # pragma: no cover if __name__ == "__main__": # pragma: no cover
from bec_lib.client import BECClient from bec_lib.client import BECClient
from bec_lib.service_config import ServiceConfig from bec_lib.service_config import ServiceConfig
config = ServiceConfig() config = ServiceConfig()
client = BECClient(config) bec_client = BECClient(config)
client.start() bec_client.start()
# Test the client_utils.py module # Test the client_utils.py module
gui = BECGuiClient() gui = BECGuiClient()

View File

@ -60,6 +60,50 @@ class RPCResponseTimeoutError(Exception):
) )
class DeletedWidgetError(Exception): ...
def check_for_deleted_widget(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
if self._gui_id not in self._registry:
raise DeletedWidgetError(f"Widget with gui_id {self._gui_id} has been deleted")
return func(self, *args, **kwargs)
return wrapper
class RPCReference:
def __init__(self, registry: dict, gui_id: str) -> None:
self._registry = registry
self._gui_id = gui_id
@check_for_deleted_widget
def __getattr__(self, name):
if name in ["_registry", "_gui_id"]:
return super().__getattribute__(name)
return self._registry[self._gui_id].__getattribute__(name)
@check_for_deleted_widget
def __getitem__(self, key):
return self._registry[self._gui_id].__getitem__(key)
def __repr__(self):
if self._gui_id not in self._registry:
return f"<Deleted widget with gui_id {self._gui_id}>"
return self._registry[self._gui_id].__repr__()
def __str__(self):
if self._gui_id not in self._registry:
return f"<Deleted widget with gui_id {self._gui_id}>"
return self._registry[self._gui_id].__str__()
def __dir__(self):
if self._gui_id not in self._registry:
return []
return self._registry[self._gui_id].__dir__()
class RPCBase: class RPCBase:
def __init__( def __init__(
self, self,
@ -182,7 +226,11 @@ class RPCBase:
cls = getattr(client, cls) cls = getattr(client, cls)
# print(msg_result) # print(msg_result)
return cls(parent=self, **msg_result) ret = cls(parent=self, **msg_result)
self._root._ipython_registry[ret._gui_id] = ret
obj = RPCReference(self._root._ipython_registry, ret._gui_id)
return obj
# return ret
return msg_result return msg_result
def _gui_is_alive(self): def _gui_is_alive(self):

View File

@ -17,6 +17,20 @@ if TYPE_CHECKING: # pragma: no cover
logger = bec_logger.logger logger = bec_logger.logger
def broadcast_update(func):
"""
Decorator to broadcast updates to the RPCRegister whenever a new RPC object is added or removed.
"""
@wraps(func)
def wrapper(self, *args, **kwargs):
result = func(self, *args, **kwargs)
self.broadcast()
return result
return wrapper
class RPCRegister: class RPCRegister:
""" """
A singleton class that keeps track of all the RPC objects registered in the system for CLI usage. A singleton class that keeps track of all the RPC objects registered in the system for CLI usage.
@ -37,7 +51,9 @@ class RPCRegister:
return return
self._rpc_register = WeakValueDictionary() self._rpc_register = WeakValueDictionary()
self._initialized = True self._initialized = True
self.callbacks = []
@broadcast_update
def add_rpc(self, rpc: QObject): def add_rpc(self, rpc: QObject):
""" """
Add an RPC object to the register. Add an RPC object to the register.
@ -49,6 +65,7 @@ class RPCRegister:
raise ValueError("RPC object must have a 'gui_id' attribute.") raise ValueError("RPC object must have a 'gui_id' attribute.")
self._rpc_register[rpc.gui_id] = rpc self._rpc_register[rpc.gui_id] = rpc
@broadcast_update
def remove_rpc(self, rpc: str): def remove_rpc(self, rpc: str):
""" """
Remove an RPC object from the register. Remove an RPC object from the register.
@ -97,6 +114,25 @@ class RPCRegister:
widgets = [rpc for rpc in self._rpc_register.values() if isinstance(rpc, cls)] widgets = [rpc for rpc in self._rpc_register.values() if isinstance(rpc, cls)]
return [widget._name for widget in widgets] return [widget._name for widget in widgets]
def broadcast(self):
"""
Broadcast the update to all the callbacks.
"""
# print("Broadcasting")
connections = self.list_all_connections()
for callback in self.callbacks:
callback(connections)
def add_callback(self, callback: Callable[[dict], None]):
"""
Add a callback that will be called whenever the registry is updated.
Args:
callback(Callable[[dict], None]): The callback to be added. It should accept a dictionary of all the
registered RPC objects as an argument.
"""
self.callbacks.append(callback)
@classmethod @classmethod
def reset_singleton(cls): def reset_singleton(cls):
""" """

View File

@ -69,6 +69,7 @@ class BECWidgetsCLIServer:
self.gui_id = gui_id self.gui_id = gui_id
# register broadcast callback # register broadcast callback
self.rpc_register = RPCRegister() self.rpc_register = RPCRegister()
self.rpc_register.add_callback(self.broadcast_registry_update)
self.gui = gui_class(parent=None, name=gui_class_id, gui_id=gui_class_id) self.gui = gui_class(parent=None, name=gui_class_id, gui_id=gui_class_id)
# self.rpc_register.add_rpc(self.gui) # self.rpc_register.add_rpc(self.gui)
@ -139,13 +140,15 @@ class BECWidgetsCLIServer:
def serialize_object(self, obj): def serialize_object(self, obj):
if isinstance(obj, BECConnector): if isinstance(obj, BECConnector):
config = {} # obj.config.model_dump()
config["parent_id"] = obj.parent_id
return { return {
"gui_id": obj.gui_id, "gui_id": obj.gui_id,
"name": ( "name": (
obj._name if hasattr(obj, "_name") else obj.__class__.__name__ obj._name if hasattr(obj, "_name") else obj.__class__.__name__
), # pylint: disable=protected-access ), # pylint: disable=protected-access
"widget_class": obj.__class__.__name__, "widget_class": obj.__class__.__name__,
"config": obj.config.model_dump(), "config": config,
"__rpc__": True, "__rpc__": True,
} }
return obj return obj
@ -161,6 +164,22 @@ class BECWidgetsCLIServer:
except RedisError as exc: except RedisError as exc:
logger.error(f"Error while emitting heartbeat: {exc}") logger.error(f"Error while emitting heartbeat: {exc}")
def broadcast_registry_update(self, connections: dict):
"""
Broadcast the updated registry to all clients.
"""
# We only need to broadcast the dock areas
data = {key: self.serialize_object(val) for key, val in connections.items()}
# logger.info(f"All registered connections: {list(connections.keys())}")
for k, v in data.items():
logger.info(f"key: {k}, value: {v}")
self.client.connector.xadd(
MessageEndpoints.gui_registry_state(self.gui_id),
msg_dict={"data": messages.GUIRegistryStateMessage(state=data)},
max_size=1, # only single message in stream
)
def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector
logger.info(f"Shutting down server with gui_id: {self.gui_id}") logger.info(f"Shutting down server with gui_id: {self.gui_id}")
self.status = messages.BECStatus.IDLE self.status = messages.BECStatus.IDLE

View File

@ -82,6 +82,7 @@ class BECConnector:
config: ConnectionConfig | None = None, config: ConnectionConfig | None = None,
gui_id: str | None = None, gui_id: str | None = None,
name: str | None = None, name: str | None = None,
parent_id: str | None = None,
): ):
# BEC related connections # BEC related connections
self.bec_dispatcher = BECDispatcher(client=client) self.bec_dispatcher = BECDispatcher(client=client)
@ -110,14 +111,12 @@ class BECConnector:
) )
self.config = ConnectionConfig(widget_class=self.__class__.__name__) self.config = ConnectionConfig(widget_class=self.__class__.__name__)
# I feel that we should not allow BECConnector to be created with a custom gui_id self.parent_id = parent_id
# because this would break with the logic in the RPCRegister of retrieving widgets by type # If the gui_id is passed, it should be respected. However, this should be revisted since
# iterating over all widgets and checkinf if the register widget starts with the string that is passsed. # the gui_id has to be unique, and may no longer be.
# If the gui_id is randomly generated, this would break since that widget would have a
# gui_id that is generated in a different way.
if gui_id: if gui_id:
self.config.gui_id = gui_id self.config.gui_id = gui_id
self.gui_id: str = gui_id self.gui_id: str = gui_id # Keep namespace in sync
else: else:
self.gui_id: str = self.config.gui_id # type: ignore self.gui_id: str = self.config.gui_id # type: ignore
if name is None: if name is None:
@ -319,6 +318,7 @@ class BECConnector:
self.deleteLater() self.deleteLater()
else: else:
self.rpc_register.remove_rpc(self) self.rpc_register.remove_rpc(self)
self.rpc_register.broadcast()
def get_config(self, dict_output: bool = True) -> dict | BaseModel: def get_config(self, dict_output: bool = True) -> dict | BaseModel:
""" """

View File

@ -34,6 +34,7 @@ class BECWidget(BECConnector):
theme_update: bool = False, theme_update: bool = False,
name: str | None = None, name: str | None = None,
parent_dock: BECDock | None = None, parent_dock: BECDock | None = None,
parent_id: str | None = None,
**kwargs, **kwargs,
): ):
""" """
@ -56,7 +57,9 @@ class BECWidget(BECConnector):
if not isinstance(self, QWidget): if not isinstance(self, QWidget):
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget") raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
super().__init__(client=client, config=config, gui_id=gui_id, name=name) super().__init__(
client=client, config=config, gui_id=gui_id, name=name, parent_id=parent_id
)
self._parent_dock = parent_dock self._parent_dock = parent_dock
app = QApplication.instance() app = QApplication.instance()
if not hasattr(app, "theme"): if not hasattr(app, "theme"):

View File

@ -131,6 +131,7 @@ class BECDock(BECWidget, Dock):
self, self,
parent: QWidget | None = None, parent: QWidget | None = None,
parent_dock_area: BECDockArea | None = None, parent_dock_area: BECDockArea | None = None,
parent_id: str | None = None,
config: DockConfig | None = None, config: DockConfig | None = None,
name: str | None = None, name: str | None = None,
client=None, client=None,
@ -149,7 +150,7 @@ class BECDock(BECWidget, Dock):
config = DockConfig(**config) config = DockConfig(**config)
self.config = config self.config = config
super().__init__( super().__init__(
client=client, config=config, gui_id=gui_id, name=name client=client, config=config, gui_id=gui_id, name=name, parent_id=parent_id
) # Name was checked and created in BEC Widget ) # Name was checked and created in BEC Widget
label = CustomDockLabel(text=name, closable=closable) label = CustomDockLabel(text=name, closable=closable)
Dock.__init__(self, name=name, label=label, parent=self, **kwargs) Dock.__init__(self, name=name, label=label, parent=self, **kwargs)
@ -324,18 +325,24 @@ class BECDock(BECWidget, Dock):
if isinstance(widget, str): if isinstance(widget, str):
widget = cast( widget = cast(
BECWidget, BECWidget,
widget_handler.create_widget(widget_type=widget, name=name, parent_dock=self), widget_handler.create_widget(
widget_type=widget, name=name, parent_dock=self, parent_id=self.gui_id
),
) )
else: else:
widget._name = name # pylint: disable=protected-access widget._name = name # pylint: disable=protected-access
self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan) self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
if hasattr(widget, "config"): if hasattr(widget, "config"):
self.config.widgets[widget.gui_id] = widget.config widget.config.gui_id = widget.gui_id
self.config.widgets[widget._name] = widget.config # pylint: disable=protected-access
self._broadcast_update()
return widget return widget
def _broadcast_update(self):
rpc_register = RPCRegister()
rpc_register.broadcast()
def move_widget(self, widget: QWidget, new_row: int, new_col: int): def move_widget(self, widget: QWidget, new_row: int, new_col: int):
""" """
Move a widget to a new position in the layout. Move a widget to a new position in the layout.

View File

@ -357,7 +357,7 @@ class BECDockArea(BECWidget, QWidget):
else: # Name is not provided else: # Name is not provided
name = WidgetContainerUtils.generate_unique_name(name="dock", list_of_names=dock_names) name = WidgetContainerUtils.generate_unique_name(name="dock", list_of_names=dock_names)
dock = BECDock(name=name, parent_dock_area=self, closable=closable) dock = BECDock(name=name, parent_dock_area=self, parent_id=self.gui_id, closable=closable)
dock.config.position = position dock.config.position = position
self.config.docks[dock.name()] = dock.config self.config.docks[dock.name()] = dock.config
# The dock.name is equal to the name passed to BECDock # The dock.name is equal to the name passed to BECDock
@ -381,8 +381,14 @@ class BECDockArea(BECWidget, QWidget):
self.update() self.update()
if floating: if floating:
dock.detach() dock.detach()
# Run broadcast update
self._broadcast_update()
return dock return dock
def _broadcast_update(self):
rpc_register = RPCRegister()
rpc_register.broadcast()
def detach_dock(self, dock_name: str) -> BECDock: def detach_dock(self, dock_name: str) -> BECDock:
""" """
Undock a dock from the dock area. Undock a dock from the dock area.

View File

@ -76,6 +76,7 @@ class BECMotorMap(BECPlotBase):
config: Optional[MotorMapConfig] = None, config: Optional[MotorMapConfig] = None,
client=None, client=None,
gui_id: Optional[str] = None, gui_id: Optional[str] = None,
**kwargs,
): ):
if config is None: if config is None:
config = MotorMapConfig(widget_class=self.__class__.__name__) config = MotorMapConfig(widget_class=self.__class__.__name__)

View File

@ -274,4 +274,4 @@ class BECCurve(BECConnector, pg.PlotDataItem):
"""Remove the curve from the plot.""" """Remove the curve from the plot."""
# self.parent_item.removeItem(self) # self.parent_item.removeItem(self)
self.parent_item.remove_curve(self.name()) self.parent_item.remove_curve(self.name())
self.rpc_register.remove_rpc(self) super().remove()

View File

@ -325,4 +325,4 @@ class Curve(BECConnector, pg.PlotDataItem):
"""Remove the curve from the plot.""" """Remove the curve from the plot."""
# self.parent_item.removeItem(self) # self.parent_item.removeItem(self)
self.parent_item.remove_curve(self.name()) self.parent_item.remove_curve(self.name())
self.rpc_register.remove_rpc(self) super().remove()

View File

@ -1,81 +1,59 @@
"""This module contains fixtures that are used in the end-2-end tests."""
import random import random
import time
from contextlib import contextmanager
import pytest import pytest
from bec_lib.endpoints import MessageEndpoints
from bec_widgets.cli.client_utils import BECGuiClient, _start_plot_process from bec_widgets.cli.client_utils import BECGuiClient
from bec_widgets.utils import BECDispatcher
from bec_widgets.widgets.containers.figure import BECFigure # pylint: disable=unused-argument
# pylint: disable=redefined-outer-name
# make threads check in autouse, **will be executed at the end**; better than
# having it in fixtures for each test, since it prevents from needing to
# 'manually' shutdown bec_client_lib (for example) to make it happy, whereas
# whereas in fact bec_client_lib makes its on cleanup
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def threads_check_fixture(threads_check): def threads_check_fixture(threads_check):
"""
Fixture to check if threads are still alive at the end of the test.
This should always run to avoid leaked threads within our application.
The fixture is set to autouse, meaning it will run for every test.
"""
return return
@pytest.fixture @pytest.fixture
def gui_id(): def gui_id():
return f"figure_{random.randint(0,100)}" # make a new gui id each time, to ensure no 'gui is alive' zombie key can perturbate """New gui id each time, to ensure no 'gui is alive' zombie key can perturbate"""
return f"figure_{random.randint(0,100)}"
@contextmanager
def plot_server(gui_id, klass, client_lib):
dispatcher = BECDispatcher(client=client_lib) # Has to init singleton with fixture client
process, _ = _start_plot_process(
gui_id, klass, gui_class_id="bec", config=client_lib._client._service_config.config_path
)
try:
while client_lib._client.connector.get(MessageEndpoints.gui_heartbeat(gui_id)) is None:
time.sleep(0.3)
yield gui_id
finally:
process.terminate()
process.wait()
dispatcher.disconnect_all()
dispatcher.reset_singleton()
@pytest.fixture
def connected_client_figure(gui_id, bec_client_lib):
with plot_server(gui_id, BECFigure, bec_client_lib) as server:
yield server
@pytest.fixture @pytest.fixture
def connected_client_gui_obj(gui_id, bec_client_lib): def connected_client_gui_obj(gui_id, bec_client_lib):
"""
Fixture to create a new BECGuiClient object and start a server in the background.
This fixture should be used if a new gui instance is needed for each test.
"""
gui = BECGuiClient(gui_id=gui_id) gui = BECGuiClient(gui_id=gui_id)
try: try:
gui.start(wait=True) gui.start(wait=True)
# gui._start_server(wait=True)
yield gui yield gui
finally: finally:
gui.kill_server() gui.kill_server()
@pytest.fixture @pytest.fixture(scope="session")
def connected_client_dock(gui_id, bec_client_lib): def connected_gui_with_scope_session(gui_id, bec_client_lib):
"""
Fixture to create a new BECGuiClient object and start a server in the background.
This fixture is scoped to the session, meaning it remains alive for all tests in the session.
We can use this fixture to create a gui object that is used across multiple tests, and
simulate a real-world scenario where the gui is not restarted for each test.
"""
gui = BECGuiClient(gui_id=gui_id) gui = BECGuiClient(gui_id=gui_id)
gui._auto_updates_enabled = False
try: try:
gui.start(wait=True) gui.start(wait=True)
gui.window_list[0] yield gui
yield gui.window_list[0]
finally:
gui.kill_server()
@pytest.fixture
def connected_client_dock_w_auto_updates(gui_id, bec_client_lib):
gui = BECGuiClient(gui_id=gui_id)
try:
gui._start_server(wait=True)
yield gui, gui.window_list[0]
finally: finally:
gui.kill_server() gui.kill_server()

View File

@ -1,378 +1,284 @@
# import time import time
# import numpy as np import numpy as np
# import pytest import pytest
# from bec_lib.endpoints import MessageEndpoints from bec_lib.endpoints import MessageEndpoints
# from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform
# from bec_widgets.tests.utils import check_remote_data_size from bec_widgets.cli.rpc.rpc_base import RPCReference
# from bec_widgets.utils import Colors from bec_widgets.tests.utils import check_remote_data_size
from bec_widgets.utils import Colors
# # pylint: disable=unused-argument
# # pylint: disable=redefined-outer-name # pylint: disable=unused-argument
# # pylint: disable=too-many-locals # pylint: disable=redefined-outer-name
# pylint: disable=too-many-locals
# pylint: disable=protected-access
# def test_rpc_add_dock_with_figure_e2e(qtbot, bec_client_lib, connected_client_dock):
# # BEC client shortcuts
# dock = connected_client_dock def test_gui_rpc_registry(qtbot, connected_client_gui_obj):
# client = bec_client_lib gui = connected_client_gui_obj
# dev = client.device_manager.devices dock_area = gui.new("cool_dock_area")
# scans = client.scans
# queue = client.queue def check_dock_area_registered():
return dock_area._gui_id in gui._registry_state
# # Create 3 docks
# d0 = dock.add_dock("dock_0") qtbot.waitUntil(check_dock_area_registered, timeout=5000)
# d1 = dock.add_dock("dock_1") assert hasattr(gui, "cool_dock_area")
# d2 = dock.add_dock("dock_2")
dock = dock_area.new("dock_0")
# dock_config = dock._config_dict
# assert len(dock_config["docks"]) == 3 def check_dock_registered():
# # Add 3 figures with some widgets dock_dict = (
# fig0 = d0.add_widget("BECFigure") gui._registry_state.get(dock_area._gui_id, {}).get("config", {}).get("docks", {})
# fig1 = d1.add_widget("BECFigure") )
# fig2 = d2.add_widget("BECFigure") return len(dock_dict) == 1
# dock_config = dock._config_dict qtbot.waitUntil(check_dock_registered, timeout=5000)
# assert len(dock_config["docks"]) == 3 assert hasattr(gui.cool_dock_area, "dock_0")
# assert len(dock_config["docks"]["dock_0"]["widgets"]) == 1
# assert len(dock_config["docks"]["dock_1"]["widgets"]) == 1 # assert hasattr(dock_area, "dock_0")
# assert len(dock_config["docks"]["dock_2"]["widgets"]) == 1
# assert fig1.__class__.__name__ == "BECFigure" def test_rpc_add_dock_with_figure_e2e(qtbot, bec_client_lib, connected_client_gui_obj):
# assert fig1.__class__ == BECFigure
# assert fig2.__class__.__name__ == "BECFigure" gui = connected_client_gui_obj
# assert fig2.__class__ == BECFigure # BEC client shortcuts
dock = gui.bec
# mm = fig0.motor_map("samx", "samy") client = bec_client_lib
# plt = fig1.plot(x_name="samx", y_name="bpm4i") dev = client.device_manager.devices
# im = fig2.image("eiger") scans = client.scans
queue = client.queue
# assert mm.__class__.__name__ == "BECMotorMap"
# assert mm.__class__ == BECMotorMap # Create 3 docks
# assert plt.__class__.__name__ == "BECWaveform" d0 = dock.new("dock_0")
# assert plt.__class__ == BECWaveform d1 = dock.new("dock_1")
# assert im.__class__.__name__ == "BECImageShow" d2 = dock.new("dock_2")
# assert im.__class__ == BECImageShow
# Check that callback for dock_registry is done
# assert mm._config_dict["signals"] == { def check_docks_registered():
# "dap": None, dock_register = dock._parent._registry_state.get(dock._gui_id, None)
# "source": "device_readback", if dock_register is not None:
# "x": { n_docks = dock_register.get("config", {}).get("docks", {})
# "name": "samx", return len(n_docks) == 3
# "entry": "samx", return False
# "unit": None, # raise AssertionError("Docks not registered yet")
# "modifier": None,
# "limits": [-50.0, 50.0],
# },
# "y": {
# "name": "samy",
# "entry": "samy",
# "unit": None,
# "modifier": None,
# "limits": [-50.0, 50.0],
# },
# "z": None,
# }
# assert plt._config_dict["curves"]["bpm4i-bpm4i"]["signals"] == {
# "dap": None,
# "source": "scan_segment",
# "x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
# "y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},
# "z": None,
# }
# assert im._config_dict["images"]["eiger"]["monitor"] == "eiger"
# # check initial position of motor map
# initial_pos_x = dev.samx.read()["samx"]["value"]
# initial_pos_y = dev.samy.read()["samy"]["value"]
# # Try to make a scan
# status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
# status.wait()
# # plot
# item = queue.scan_storage.storage[-1]
# plt_last_scan_data = item.live_data if hasattr(item, "live_data") else item.data
# num_elements = 10
# plot_name = "bpm4i-bpm4i"
# qtbot.waitUntil(lambda: check_remote_data_size(plt, plot_name, num_elements))
# plt_data = plt.get_all_data()
# assert plt_data["bpm4i-bpm4i"]["x"] == plt_last_scan_data["samx"]["samx"].val
# assert plt_data["bpm4i-bpm4i"]["y"] == plt_last_scan_data["bpm4i"]["bpm4i"].val
# # image
# last_image_device = client.connector.get_last(MessageEndpoints.device_monitor_2d("eiger"))[
# "data"
# ].data
# time.sleep(0.5)
# last_image_plot = im.images[0].get_data()
# np.testing.assert_equal(last_image_device, last_image_plot)
# # motor map
# final_pos_x = dev.samx.read()["samx"]["value"]
# final_pos_y = dev.samy.read()["samy"]["value"]
# # check final coordinates of motor map
# motor_map_data = mm.get_data()
# np.testing.assert_equal(
# [motor_map_data["x"][0], motor_map_data["y"][0]], [initial_pos_x, initial_pos_y]
# )
# np.testing.assert_equal(
# [motor_map_data["x"][-1], motor_map_data["y"][-1]], [final_pos_x, final_pos_y]
# )
# def test_dock_manipulations_e2e(connected_client_dock):
# dock = connected_client_dock
# d0 = dock.add_dock("dock_0")
# d1 = dock.add_dock("dock_1")
# d2 = dock.add_dock("dock_2")
# dock_config = dock._config_dict
# assert len(dock_config["docks"]) == 3
# d0.detach()
# dock.detach_dock("dock_2")
# dock_config = dock._config_dict
# assert len(dock_config["docks"]) == 3
# assert len(dock.temp_areas) == 2
# d0.attach()
# dock_config = dock._config_dict
# assert len(dock_config["docks"]) == 3
# assert len(dock.temp_areas) == 1
# d2.remove()
# dock_config = dock._config_dict
# assert ["dock_0", "dock_1"] == list(dock_config["docks"])
# dock.clear_all()
# dock_config = dock._config_dict
# assert len(dock_config["docks"]) == 0
# assert len(dock.temp_areas) == 0
# def test_ring_bar(connected_client_dock):
# dock = connected_client_dock
# d0 = dock.add_dock(name="dock_0")
# bar = d0.add_widget("RingProgressBar")
# assert bar.__class__.__name__ == "RingProgressBar"
# bar.set_number_of_bars(5) # Waii until docks are registered
# bar.set_colors_from_map("viridis") qtbot.waitUntil(check_docks_registered, timeout=5000)
# bar.set_value([10, 20, 30, 40, 50]) qtbot.wait(500)
assert len(dock.panels) == 3
assert hasattr(gui.bec, "dock_0")
# bar_config = bar._config_dict # Add 3 figures with some widgets
fig0 = d0.new("BECFigure")
fig1 = d1.new("BECFigure")
fig2 = d2.new("BECFigure")
# expected_colors_light = [ def check_fig2_registered():
# list(color) for color in Colors.golden_angle_color("viridis", 5, "RGB", theme="light") # return hasattr(d2, "BECFigure_2")
# ] dock_config = dock._parent._registry_state[dock._gui_id]["config"]["docks"].get(
# expected_colors_dark = [ d2.widget_name, {}
# list(color) for color in Colors.golden_angle_color("viridis", 5, "RGB", theme="dark") )
# ] if dock_config:
# bar_colors = [ring._config_dict["color"] for ring in bar.rings] n_widgets = dock_config.get("widgets", {})
# bar_values = [ring._config_dict["value"] for ring in bar.rings] if any(widget_name.startswith("BECFigure") for widget_name in n_widgets.keys()):
# assert bar_config["num_bars"] == 5 return True
# assert bar_values == [10, 20, 30, 40, 50] raise AssertionError("Figure not registered yet")
# assert bar_colors == expected_colors_light or bar_colors == expected_colors_dark
qtbot.waitUntil(check_fig2_registered, timeout=5000)
# def test_ring_bar_scan_update(bec_client_lib, connected_client_dock): assert len(d0.element_list) == 1
# dock = connected_client_dock assert len(d1.element_list) == 1
assert len(d2.element_list) == 1
# d0 = dock.add_dock("dock_0") assert fig1.__class__.__name__ == "RPCReference"
assert fig1.__class__ == RPCReference
assert gui._ipython_registry[fig1._gui_id].__class__ == BECFigure
assert fig2.__class__.__name__ == "RPCReference"
assert fig2.__class__ == RPCReference
assert gui._ipython_registry[fig2._gui_id].__class__ == BECFigure
# bar = d0.add_widget("RingProgressBar") mm = fig0.motor_map("samx", "samy")
plt = fig1.plot(x_name="samx", y_name="bpm4i")
im = fig2.image("eiger")
# client = bec_client_lib assert mm.__class__.__name__ == "RPCReference"
# dev = client.device_manager.devices assert mm.__class__ == RPCReference
# dev.samx.tolerance.set(0) assert plt.__class__.__name__ == "RPCReference"
# dev.samy.tolerance.set(0) assert plt.__class__ == RPCReference
# scans = client.scans assert im.__class__.__name__ == "RPCReference"
assert im.__class__ == RPCReference
# status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
# status.wait()
# bar_config = bar._config_dict def test_dock_manipulations_e2e(qtbot, connected_client_gui_obj):
# assert bar_config["num_bars"] == 1 gui = connected_client_gui_obj
# assert bar_config["rings"][0]["value"] == 10 dock = gui.bec
# assert bar_config["rings"][0]["min_value"] == 0
# assert bar_config["rings"][0]["max_value"] == 10
# status = scans.grid_scan(dev.samx, -5, 5, 4, dev.samy, -10, 10, 4, relative=True, exp_time=0.1) d0 = dock.new("dock_0")
# status.wait() d1 = dock.new("dock_1")
d2 = dock.new("dock_2")
# bar_config = bar._config_dict assert hasattr(gui.bec, "dock_0")
# assert bar_config["num_bars"] == 1 assert hasattr(gui.bec, "dock_1")
# assert bar_config["rings"][0]["value"] == 16 assert hasattr(gui.bec, "dock_2")
# assert bar_config["rings"][0]["min_value"] == 0 assert len(gui.bec.panels) == 3
# assert bar_config["rings"][0]["max_value"] == 16
# init_samx = dev.samx.read()["samx"]["value"] d0.detach()
# init_samy = dev.samy.read()["samy"]["value"] dock.detach_dock("dock_2")
# final_samx = init_samx + 5 # How can we properly check that the dock is detached?
# final_samy = init_samy + 10 assert len(gui.bec.panels) == 3
d0.attach()
assert len(gui.bec.panels) == 3
# dev.samx.velocity.put(5) def wait_for_dock_removed():
# dev.samy.velocity.put(5) dock_config = gui._registry_state[gui.bec._gui_id]["config"]["docks"]
return len(dock_config.keys()) == 2
# status = scans.umv(dev.samx, 5, dev.samy, 10, relative=True)
# status.wait() d2.remove()
qtbot.waitUntil(wait_for_dock_removed, timeout=5000)
# bar_config = bar._config_dict assert len(gui.bec.panels) == 2
# assert bar_config["num_bars"] == 2
# assert bar_config["rings"][0]["value"] == final_samx def wait_for_docks_removed():
# assert bar_config["rings"][1]["value"] == final_samy dock_config = gui._registry_state[gui.bec._gui_id]["config"]["docks"]
# assert bar_config["rings"][0]["min_value"] == init_samx return len(dock_config.keys()) == 0
# assert bar_config["rings"][0]["max_value"] == final_samx
# assert bar_config["rings"][1]["min_value"] == init_samy dock.clear_all()
# assert bar_config["rings"][1]["max_value"] == final_samy qtbot.waitUntil(wait_for_docks_removed, timeout=5000)
assert len(gui.bec.panels) == 0
# def test_auto_update(bec_client_lib, connected_client_dock_w_auto_updates, qtbot):
# client = bec_client_lib def test_ring_bar(connected_client_dock):
# dev = client.device_manager.devices dock = connected_client_dock
# scans = client.scans
# queue = client.queue d0 = dock.add_dock(name="dock_0")
# gui, dock = connected_client_dock_w_auto_updates
# auto_updates = gui.auto_updates bar = d0.add_widget("RingProgressBar")
assert bar.__class__.__name__ == "RingProgressBar"
# def get_default_figure():
# return auto_updates.get_default_figure() plt = get_default_figure()
# plt = get_default_figure() gui.selected_device = "bpm4i"
# gui.selected_device = "bpm4i" status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
status.wait()
# status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
# status.wait() # get data from curves
widgets = plt.widget_list
# # get data from curves qtbot.waitUntil(lambda: len(plt.widget_list) > 0, timeout=5000)
# widgets = plt.widget_list
# qtbot.waitUntil(lambda: len(plt.widget_list) > 0, timeout=5000) item = queue.scan_storage.storage[-1]
last_scan_data = item.live_data if hasattr(item, "live_data") else item.data
# item = queue.scan_storage.storage[-1]
# last_scan_data = item.live_data if hasattr(item, "live_data") else item.data num_elements = 10
# num_elements = 10 plot_name = f"Scan {status.scan.scan_number} - {dock.selected_device}"
# plot_name = f"Scan {status.scan.scan_number} - {dock.selected_device}" qtbot.waitUntil(lambda: check_remote_data_size(widgets[0], plot_name, num_elements))
plt_data = widgets[0].get_all_data()
# qtbot.waitUntil(lambda: check_remote_data_size(widgets[0], plot_name, num_elements))
# plt_data = widgets[0].get_all_data() # check plotted data
assert (
# # check plotted data plt_data[f"Scan {status.scan.scan_number} - bpm4i"]["x"]
# assert ( == last_scan_data["samx"]["samx"].val
# plt_data[f"Scan {status.scan.scan_number} - bpm4i"]["x"] )
# == last_scan_data["samx"]["samx"].val assert (
# ) plt_data[f"Scan {status.scan.scan_number} - bpm4i"]["y"]
# assert ( == last_scan_data["bpm4i"]["bpm4i"].val
# plt_data[f"Scan {status.scan.scan_number} - bpm4i"]["y"] )
# == last_scan_data["bpm4i"]["bpm4i"].val
# ) status = scans.grid_scan(
dev.samx, -10, 10, 5, dev.samy, -5, 5, 5, exp_time=0.05, relative=False
# status = scans.grid_scan( )
# dev.samx, -10, 10, 5, dev.samy, -5, 5, 5, exp_time=0.05, relative=False status.wait()
# )
# status.wait() plt = auto_updates.get_default_figure()
widgets = plt.widget_list
# plt = auto_updates.get_default_figure()
# widgets = plt.widget_list qtbot.waitUntil(lambda: len(plt.widget_list) > 0, timeout=5000)
# qtbot.waitUntil(lambda: len(plt.widget_list) > 0, timeout=5000) item = queue.scan_storage.storage[-1]
last_scan_data = item.live_data if hasattr(item, "live_data") else item.data
# item = queue.scan_storage.storage[-1]
# last_scan_data = item.live_data if hasattr(item, "live_data") else item.data plot_name = f"Scan {status.scan.scan_number} - bpm4i"
# plot_name = f"Scan {status.scan.scan_number} - bpm4i" num_elements_bec = 25
qtbot.waitUntil(lambda: check_remote_data_size(widgets[0], plot_name, num_elements_bec))
# num_elements_bec = 25 plt_data = widgets[0].get_all_data()
# qtbot.waitUntil(lambda: check_remote_data_size(widgets[0], plot_name, num_elements_bec))
# plt_data = widgets[0].get_all_data() # check plotted data
assert (
# # check plotted data plt_data[f"Scan {status.scan.scan_number} - {dock.selected_device}"]["x"]
# assert ( == last_scan_data["samx"]["samx"].val
# plt_data[f"Scan {status.scan.scan_number} - {dock.selected_device}"]["x"] )
# == last_scan_data["samx"]["samx"].val assert (
# ) plt_data[f"Scan {status.scan.scan_number} - {dock.selected_device}"]["y"]
# assert ( == last_scan_data["samy"]["samy"].val
# plt_data[f"Scan {status.scan.scan_number} - {dock.selected_device}"]["y"] )
# == last_scan_data["samy"]["samy"].val
# )
def test_rpc_gui_obj(connected_client_gui_obj, qtbot):
gui = connected_client_gui_obj
# def test_rpc_gui_obj(connected_client_gui_obj, qtbot):
# gui = connected_client_gui_obj assert gui.selected_device is None
assert len(gui.windows) == 1
# assert gui.selected_device is None assert gui.windows["bec"] is gui.bec
# assert len(gui.windows) == 1 mw = gui.bec
# assert gui.windows["main"].widget is gui.main assert mw.__class__.__name__ == "BECDockArea"
# assert gui.windows["main"].title == "BEC Widgets"
# mw = gui.main xw = gui.new("X")
# assert mw.__class__.__name__ == "BECDockArea" assert xw.__class__.__name__ == "BECDockArea"
assert len(gui.windows) == 2
# xw = gui.new("X")
# assert xw.__class__.__name__ == "BECDockArea" gui_info = gui._dump()
# assert len(gui.windows) == 2 mw_info = gui_info[mw._gui_id]
assert mw_info["title"] == "BEC"
# gui_info = gui._dump() assert mw_info["visible"]
# mw_info = gui_info[mw._gui_id] xw_info = gui_info[xw._gui_id]
# assert mw_info["title"] == "BEC Widgets" assert xw_info["title"] == "BEC - X"
# assert mw_info["visible"] assert xw_info["visible"]
# xw_info = gui_info[xw._gui_id]
# assert xw_info["title"] == "X" gui.hide()
# assert xw_info["visible"] gui_info = gui._dump()
assert not any(windows["visible"] for windows in gui_info.values())
# gui.hide()
# gui_info = gui._dump() gui.show()
# assert not any(windows["visible"] for windows in gui_info.values()) gui_info = gui._dump()
assert all(windows["visible"] for windows in gui_info.values())
# gui.show()
# gui_info = gui._dump() assert gui._gui_is_alive()
# assert all(windows["visible"] for windows in gui_info.values()) gui._close()
assert not gui._gui_is_alive()
# assert gui.gui_is_alive() gui._start_server(wait=True)
# gui.close() assert gui._gui_is_alive()
# assert not gui.gui_is_alive() # calling start multiple times should not change anything
# gui.start_server(wait=True) gui._start_server(wait=True)
# assert gui.gui_is_alive() gui._start()
# # calling start multiple times should not change anything # gui.windows should have bec with gui_id 'bec'
# gui.start_server(wait=True) assert len(gui.windows) == 1
# gui.start() assert gui.windows["bec"]._gui_id == mw._gui_id
# # gui.windows should have main, and main dock area should have same gui_id as before # communication should work, main dock area should have same id and be visible
# assert len(gui.windows) == 1 gui_info = gui._dump()
# assert gui.windows["main"].widget._gui_id == mw._gui_id assert gui_info[mw._gui_id]["visible"]
# # communication should work, main dock area should have same id and be visible
# gui_info = gui._dump() with pytest.raises(RuntimeError):
# assert gui_info[mw._gui_id]["visible"] gui.bec.delete()
# with pytest.raises(RuntimeError): yw = gui.new("Y")
# gui.main.delete() assert len(gui.windows) == 2
yw.delete()
# yw = gui.new("Y") assert len(gui.windows) == 1
# assert len(gui.windows) == 2 # check it is really deleted on server
# yw.delete() gui_info = gui._dump()
# assert len(gui.windows) == 1 assert yw._gui_id not in gui_info
# # check it is really deleted on server
# gui_info = gui._dump()
# assert yw._gui_id not in gui_info def test_rpc_call_with_exception_in_safeslot_error_popup(connected_client_gui_obj, qtbot):
gui = connected_client_gui_obj
# def test_rpc_call_with_exception_in_safeslot_error_popup(connected_client_gui_obj, qtbot): gui.main.add_dock("test")
# gui = connected_client_gui_obj qtbot.waitUntil(lambda: len(gui.main.panels) == 2) # default_figure + test
qtbot.wait(500)
# gui.main.add_dock("test") with pytest.raises(ValueError):
# qtbot.waitUntil(lambda: len(gui.main.panels) == 2) # default_figure + test gui.bec.add_dock("test")
# qtbot.wait(500) # time.sleep(0.1)
# with pytest.raises(ValueError):
# gui.main.add_dock("test")
# # time.sleep(0.1)

View File

@ -5,8 +5,11 @@ import pytest
from bec_lib.endpoints import MessageEndpoints from bec_lib.endpoints import MessageEndpoints
from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform
from bec_widgets.cli.rpc.rpc_base import RPCReference
from bec_widgets.tests.utils import check_remote_data_size from bec_widgets.tests.utils import check_remote_data_size
# pylint: disable=protected-access
@pytest.fixture @pytest.fixture
def connected_figure(connected_client_gui_obj): def connected_figure(connected_client_gui_obj):
@ -39,12 +42,15 @@ def test_rpc_plotting_shortcuts_init_configs(connected_figure, qtbot):
# Checking if classes are correctly initialised # Checking if classes are correctly initialised
assert len(fig.widgets) == 4 assert len(fig.widgets) == 4
assert plt.__class__.__name__ == "BECWaveform" assert plt.__class__.__name__ == "RPCReference"
assert plt.__class__ == BECWaveform assert plt.__class__ == RPCReference
assert im.__class__.__name__ == "BECImageShow" assert plt._root._ipython_registry[plt._gui_id].__class__ == BECWaveform
assert im.__class__ == BECImageShow assert im.__class__.__name__ == "RPCReference"
assert motor_map.__class__.__name__ == "BECMotorMap" assert im.__class__ == RPCReference
assert motor_map.__class__ == BECMotorMap assert im._root._ipython_registry[im._gui_id].__class__ == BECImageShow
assert motor_map.__class__.__name__ == "RPCReference"
assert motor_map.__class__ == RPCReference
assert motor_map._root._ipython_registry[motor_map._gui_id].__class__ == BECMotorMap
# check if the correct devices are set # check if the correct devices are set
# plot # plot
@ -215,10 +221,11 @@ def test_dap_rpc(connected_figure, bec_client_lib, qtbot):
def test_removing_subplots(connected_figure, bec_client_lib): def test_removing_subplots(connected_figure, bec_client_lib):
fig = connected_figure fig = connected_figure
plt = fig.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel") plt = fig.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
im = fig.image(monitor="eiger") # Registry can't handle multiple subplots on one widget, BECFigure will be deprecated though
mm = fig.motor_map(motor_x="samx", motor_y="samy") # im = fig.image(monitor="eiger")
# mm = fig.motor_map(motor_x="samx", motor_y="samy")
assert len(fig.widget_list) == 3 assert len(fig.widget_list) == 1
# removing curves # removing curves
assert len(plt.curves) == 2 assert len(plt.curves) == 2
@ -229,7 +236,7 @@ def test_removing_subplots(connected_figure, bec_client_lib):
# removing all subplots from figure # removing all subplots from figure
plt.remove() plt.remove()
im.remove() # im.remove()
mm.remove() # mm.remove()
assert len(fig.widget_list) == 0 assert len(fig.widget_list) == 0