bec-widgets 1.11.0__py3-none-any.whl → 1.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
PKG-INFO CHANGED
@@ -1,9 +1,10 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 1.11.0
3
+ Version: 1.13.0
4
4
  Summary: BEC Widgets
5
5
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
6
6
  Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
7
+ License-File: LICENSE
7
8
  Classifier: Development Status :: 3 - Alpha
8
9
  Classifier: Programming Language :: Python :: 3
9
10
  Classifier: Topic :: Scientific/Engineering
@@ -21,7 +22,7 @@ Requires-Dist: qtpy~=2.4
21
22
  Provides-Extra: dev
22
23
  Requires-Dist: coverage~=7.0; extra == 'dev'
23
24
  Requires-Dist: fakeredis>=2.23.2,~=2.23; extra == 'dev'
24
- Requires-Dist: pytest-bec-e2e~=2.16; extra == 'dev'
25
+ Requires-Dist: pytest-bec-e2e<=4.0,>=2.21.4; extra == 'dev'
25
26
  Requires-Dist: pytest-qt~=4.4; extra == 'dev'
26
27
  Requires-Dist: pytest-random-order~=1.1; extra == 'dev'
27
28
  Requires-Dist: pytest-timeout~=2.2; extra == 'dev'
@@ -27,25 +27,17 @@ class AutoUpdates:
27
27
 
28
28
  def __init__(self, gui: BECDockArea):
29
29
  self.gui = gui
30
- self.msg_queue = Queue()
31
- self.auto_update_thread = None
32
- self._shutdown_sentinel = object()
33
- self.start()
34
-
35
- def start(self):
36
- """
37
- Start the auto update thread.
38
- """
39
- self.auto_update_thread = threading.Thread(target=self.process_queue)
40
- self.auto_update_thread.start()
30
+ self._default_dock = None
31
+ self._default_fig = None
41
32
 
42
33
  def start_default_dock(self):
43
34
  """
44
35
  Create a default dock for the auto updates.
45
36
  """
46
- dock = self.gui.add_dock("default_figure")
47
- dock.add_widget("BECFigure")
48
37
  self.dock_name = "default_figure"
38
+ self._default_dock = self.gui.add_dock(self.dock_name)
39
+ self._default_dock.add_widget("BECFigure")
40
+ self._default_fig = self._default_dock.widget_list[0]
49
41
 
50
42
  @staticmethod
51
43
  def get_scan_info(msg) -> ScanInfo:
@@ -73,15 +65,9 @@ class AutoUpdates:
73
65
  """
74
66
  Get the default figure from the GUI.
75
67
  """
76
- dock = self.gui.panels.get(self.dock_name, [])
77
- if not dock:
78
- return None
79
- widgets = dock.widget_list
80
- if not widgets:
81
- return None
82
- return widgets[0]
68
+ return self._default_fig
83
69
 
84
- def run(self, msg):
70
+ def do_update(self, msg):
85
71
  """
86
72
  Run the update function if enabled.
87
73
  """
@@ -90,20 +76,9 @@ class AutoUpdates:
90
76
  if msg.status != "open":
91
77
  return
92
78
  info = self.get_scan_info(msg)
93
- self.handler(info)
94
-
95
- def process_queue(self):
96
- """
97
- Process the message queue.
98
- """
99
- while True:
100
- msg = self.msg_queue.get()
101
- if msg is self._shutdown_sentinel:
102
- break
103
- self.run(msg)
79
+ return self.handler(info)
104
80
 
105
- @staticmethod
106
- def get_selected_device(monitored_devices, selected_device):
81
+ def get_selected_device(self, monitored_devices, selected_device):
107
82
  """
108
83
  Get the selected device for the plot. If no device is selected, the first
109
84
  device in the monitored devices list is selected.
@@ -120,14 +95,11 @@ class AutoUpdates:
120
95
  Default update function.
121
96
  """
122
97
  if info.scan_name == "line_scan" and info.scan_report_devices:
123
- self.simple_line_scan(info)
124
- return
98
+ return self.simple_line_scan(info)
125
99
  if info.scan_name == "grid_scan" and info.scan_report_devices:
126
- self.simple_grid_scan(info)
127
- return
100
+ return self.simple_grid_scan(info)
128
101
  if info.scan_report_devices:
129
- self.best_effort(info)
130
- return
102
+ return self.best_effort(info)
131
103
 
132
104
  def simple_line_scan(self, info: ScanInfo) -> None:
133
105
  """
@@ -137,12 +109,19 @@ class AutoUpdates:
137
109
  if not fig:
138
110
  return
139
111
  dev_x = info.scan_report_devices[0]
140
- dev_y = self.get_selected_device(info.monitored_devices, self.gui.selected_device)
112
+ selected_device = yield self.gui.selected_device
113
+ dev_y = self.get_selected_device(info.monitored_devices, selected_device)
141
114
  if not dev_y:
142
115
  return
143
- fig.clear_all()
144
- plt = fig.plot(x_name=dev_x, y_name=dev_y, label=f"Scan {info.scan_number} - {dev_y}")
145
- plt.set(title=f"Scan {info.scan_number}", x_label=dev_x, y_label=dev_y)
116
+ yield fig.clear_all()
117
+ yield fig.plot(
118
+ x_name=dev_x,
119
+ y_name=dev_y,
120
+ label=f"Scan {info.scan_number} - {dev_y}",
121
+ title=f"Scan {info.scan_number}",
122
+ x_label=dev_x,
123
+ y_label=dev_y,
124
+ )
146
125
 
147
126
  def simple_grid_scan(self, info: ScanInfo) -> None:
148
127
  """
@@ -153,12 +132,18 @@ class AutoUpdates:
153
132
  return
154
133
  dev_x = info.scan_report_devices[0]
155
134
  dev_y = info.scan_report_devices[1]
156
- dev_z = self.get_selected_device(info.monitored_devices, self.gui.selected_device)
157
- fig.clear_all()
158
- plt = fig.plot(
159
- x_name=dev_x, y_name=dev_y, z_name=dev_z, label=f"Scan {info.scan_number} - {dev_z}"
135
+ selected_device = yield self.gui.selected_device
136
+ dev_z = self.get_selected_device(info.monitored_devices, selected_device)
137
+ yield fig.clear_all()
138
+ yield fig.plot(
139
+ x_name=dev_x,
140
+ y_name=dev_y,
141
+ z_name=dev_z,
142
+ label=f"Scan {info.scan_number} - {dev_z}",
143
+ title=f"Scan {info.scan_number}",
144
+ x_label=dev_x,
145
+ y_label=dev_y,
160
146
  )
161
- plt.set(title=f"Scan {info.scan_number}", x_label=dev_x, y_label=dev_y)
162
147
 
163
148
  def best_effort(self, info: ScanInfo) -> None:
164
149
  """
@@ -168,17 +153,16 @@ class AutoUpdates:
168
153
  if not fig:
169
154
  return
170
155
  dev_x = info.scan_report_devices[0]
171
- dev_y = self.get_selected_device(info.monitored_devices, self.gui.selected_device)
156
+ selected_device = yield self.gui.selected_device
157
+ dev_y = self.get_selected_device(info.monitored_devices, selected_device)
172
158
  if not dev_y:
173
159
  return
174
- fig.clear_all()
175
- plt = fig.plot(x_name=dev_x, y_name=dev_y, label=f"Scan {info.scan_number} - {dev_y}")
176
- plt.set(title=f"Scan {info.scan_number}", x_label=dev_x, y_label=dev_y)
177
-
178
- def shutdown(self):
179
- """
180
- Shutdown the auto update thread.
181
- """
182
- self.msg_queue.put(self._shutdown_sentinel)
183
- if self.auto_update_thread:
184
- self.auto_update_thread.join()
160
+ yield fig.clear_all()
161
+ yield fig.plot(
162
+ x_name=dev_x,
163
+ y_name=dev_y,
164
+ label=f"Scan {info.scan_number} - {dev_y}",
165
+ title=f"Scan {info.scan_number}",
166
+ x_label=dev_x,
167
+ y_label=dev_y,
168
+ )
bec_widgets/cli/client.py CHANGED
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import enum
6
6
  from typing import Literal, Optional, overload
7
7
 
8
- from bec_widgets.cli.client_utils import BECGuiClientMixin, RPCBase, rpc_call
8
+ from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call
9
9
 
10
10
  # pylint: skip-file
11
11
 
@@ -342,7 +342,7 @@ class BECDock(RPCBase):
342
342
  """
343
343
 
344
344
 
345
- class BECDockArea(RPCBase, BECGuiClientMixin):
345
+ class BECDockArea(RPCBase):
346
346
  @property
347
347
  @rpc_call
348
348
  def _config_dict(self) -> "dict":
@@ -353,6 +353,13 @@ class BECDockArea(RPCBase, BECGuiClientMixin):
353
353
  dict: The configuration of the widget.
354
354
  """
355
355
 
356
+ @property
357
+ @rpc_call
358
+ def selected_device(self) -> "str":
359
+ """
360
+ None
361
+ """
362
+
356
363
  @property
357
364
  @rpc_call
358
365
  def panels(self) -> "dict[str, BECDock]":
@@ -480,6 +487,12 @@ class BECDockArea(RPCBase, BECGuiClientMixin):
480
487
  Hide all windows including floating docks.
481
488
  """
482
489
 
490
+ @rpc_call
491
+ def delete(self):
492
+ """
493
+ None
494
+ """
495
+
483
496
 
484
497
  class BECFigure(RPCBase):
485
498
  @property
@@ -7,61 +7,33 @@ import os
7
7
  import select
8
8
  import subprocess
9
9
  import threading
10
- import time
11
- import uuid
12
- from functools import wraps
10
+ from contextlib import contextmanager
11
+ from dataclasses import dataclass
13
12
  from typing import TYPE_CHECKING
14
13
 
15
- from bec_lib.client import BECClient
16
14
  from bec_lib.endpoints import MessageEndpoints
17
15
  from bec_lib.logger import bec_logger
18
16
  from bec_lib.utils.import_utils import isinstance_based_on_class_name, lazy_import, lazy_import_from
19
17
 
20
18
  import bec_widgets.cli.client as client
21
19
  from bec_widgets.cli.auto_updates import AutoUpdates
20
+ from bec_widgets.cli.rpc.rpc_base import RPCBase
22
21
 
23
22
  if TYPE_CHECKING:
23
+ from bec_lib import messages
24
+ from bec_lib.connector import MessageObject
24
25
  from bec_lib.device import DeviceBase
25
26
 
26
- messages = lazy_import("bec_lib.messages")
27
- # from bec_lib.connector import MessageObject
28
- MessageObject = lazy_import_from("bec_lib.connector", ("MessageObject",))
29
- BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
27
+ from bec_widgets.utils.bec_dispatcher import BECDispatcher
28
+ else:
29
+ messages = lazy_import("bec_lib.messages")
30
+ # from bec_lib.connector import MessageObject
31
+ MessageObject = lazy_import_from("bec_lib.connector", ("MessageObject",))
32
+ BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
30
33
 
31
34
  logger = bec_logger.logger
32
35
 
33
36
 
34
- def rpc_call(func):
35
- """
36
- A decorator for calling a function on the server.
37
-
38
- Args:
39
- func: The function to call.
40
-
41
- Returns:
42
- The result of the function call.
43
- """
44
-
45
- @wraps(func)
46
- def wrapper(self, *args, **kwargs):
47
- # we could rely on a strict type check here, but this is more flexible
48
- # moreover, it would anyway crash for objects...
49
- out = []
50
- for arg in args:
51
- if hasattr(arg, "name"):
52
- arg = arg.name
53
- out.append(arg)
54
- args = tuple(out)
55
- for key, val in kwargs.items():
56
- if hasattr(val, "name"):
57
- kwargs[key] = val.name
58
- if not self.gui_is_alive():
59
- raise RuntimeError("GUI is not alive")
60
- return self._run_rpc(func.__name__, *args, **kwargs)
61
-
62
- return wrapper
63
-
64
-
65
37
  def _get_output(process, logger) -> None:
66
38
  log_func = {process.stdout: logger.debug, process.stderr: logger.error}
67
39
  stream_buffer = {process.stdout: [], process.stderr: []}
@@ -132,29 +104,79 @@ class RepeatTimer(threading.Timer):
132
104
  self.function(*self.args, **self.kwargs)
133
105
 
134
106
 
135
- class BECGuiClientMixin:
107
+ @contextmanager
108
+ def wait_for_server(client):
109
+ timeout = client._startup_timeout
110
+ if not timeout:
111
+ if client.gui_is_alive():
112
+ # there is hope, let's wait a bit
113
+ timeout = 1
114
+ else:
115
+ raise RuntimeError("GUI is not alive")
116
+ try:
117
+ if client._gui_started_event.wait(timeout=timeout):
118
+ client._gui_started_timer.cancel()
119
+ client._gui_started_timer.join()
120
+ else:
121
+ raise TimeoutError("Could not connect to GUI server")
122
+ finally:
123
+ # after initial waiting period, do not wait so much any more
124
+ # (only relevant if GUI didn't start)
125
+ client._startup_timeout = 0
126
+ yield
127
+
128
+
129
+ ### ----------------------------
130
+ ### NOTE
131
+ ### it is far easier to extend the 'delete' method on the client side,
132
+ ### to know when the client is deleted, rather than listening to server
133
+ ### to get notified. However, 'generate_cli.py' cannot add extra stuff
134
+ ### in the generated client module. So, here a class with the same name
135
+ ### is created, and client module is patched.
136
+ class BECDockArea(client.BECDockArea):
137
+ def delete(self):
138
+ if self is BECGuiClient._top_level["main"].widget:
139
+ raise RuntimeError("Cannot delete main window")
140
+ super().delete()
141
+ try:
142
+ del BECGuiClient._top_level[self._gui_id]
143
+ except KeyError:
144
+ # if a dock area is not at top level
145
+ pass
146
+
147
+
148
+ client.BECDockArea = BECDockArea
149
+ ### ----------------------------
150
+
151
+
152
+ @dataclass
153
+ class WidgetDesc:
154
+ title: str
155
+ widget: BECDockArea
156
+
157
+
158
+ class BECGuiClient(RPCBase):
159
+ _top_level = {}
160
+
136
161
  def __init__(self, **kwargs) -> None:
137
162
  super().__init__(**kwargs)
138
163
  self._auto_updates_enabled = True
139
164
  self._auto_updates = None
165
+ self._startup_timeout = 0
140
166
  self._gui_started_timer = None
141
167
  self._gui_started_event = threading.Event()
142
168
  self._process = None
143
169
  self._process_output_processing_thread = None
144
- self._target_endpoint = MessageEndpoints.scan_status()
145
- self._selected_device = None
146
170
 
147
171
  @property
148
- def auto_updates(self):
149
- if self._auto_updates_enabled:
150
- self._gui_started_event.wait()
151
- return self._auto_updates
172
+ def windows(self):
173
+ return self._top_level
152
174
 
153
- def shutdown_auto_updates(self):
175
+ @property
176
+ def auto_updates(self):
154
177
  if self._auto_updates_enabled:
155
- if self._auto_updates is not None:
156
- self._auto_updates.shutdown()
157
- self._auto_updates = None
178
+ with wait_for_server(self):
179
+ return self._auto_updates
158
180
 
159
181
  def _get_update_script(self) -> AutoUpdates | None:
160
182
  eps = imd.entry_points(group="bec.widgets.auto_updates")
@@ -175,49 +197,59 @@ class BECGuiClientMixin:
175
197
  """
176
198
  Selected device for the plot.
177
199
  """
178
- return self._selected_device
200
+ auto_update_config_ep = MessageEndpoints.gui_auto_update_config(self._gui_id)
201
+ auto_update_config = self._client.connector.get(auto_update_config_ep)
202
+ if auto_update_config:
203
+ return auto_update_config.selected_device
204
+ return None
179
205
 
180
206
  @selected_device.setter
181
207
  def selected_device(self, device: str | DeviceBase):
182
208
  if isinstance_based_on_class_name(device, "bec_lib.device.DeviceBase"):
183
- self._selected_device = device.name
209
+ self._client.connector.set_and_publish(
210
+ MessageEndpoints.gui_auto_update_config(self._gui_id),
211
+ messages.GUIAutoUpdateConfigMessage(selected_device=device.name),
212
+ )
184
213
  elif isinstance(device, str):
185
- self._selected_device = device
214
+ self._client.connector.set_and_publish(
215
+ MessageEndpoints.gui_auto_update_config(self._gui_id),
216
+ messages.GUIAutoUpdateConfigMessage(selected_device=device),
217
+ )
186
218
  else:
187
219
  raise ValueError("Device must be a string or a device object")
188
220
 
189
221
  def _start_update_script(self) -> None:
190
- self._client.connector.register(
191
- self._target_endpoint, cb=self._handle_msg_update, parent=self
192
- )
222
+ self._client.connector.register(MessageEndpoints.scan_status(), cb=self._handle_msg_update)
193
223
 
194
- @staticmethod
195
- def _handle_msg_update(msg: MessageObject, parent: BECGuiClientMixin) -> None:
196
- if parent.auto_updates is not None:
224
+ def _handle_msg_update(self, msg: MessageObject) -> None:
225
+ if self.auto_updates is not None:
197
226
  # pylint: disable=protected-access
198
- parent._update_script_msg_parser(msg.value)
227
+ return self._update_script_msg_parser(msg.value)
199
228
 
200
229
  def _update_script_msg_parser(self, msg: messages.BECMessage) -> None:
201
230
  if isinstance(msg, messages.ScanStatusMessage):
202
231
  if not self.gui_is_alive():
203
232
  return
204
233
  if self._auto_updates_enabled:
205
- self.auto_updates.msg_queue.put(msg)
234
+ return self.auto_updates.do_update(msg)
206
235
 
207
236
  def _gui_post_startup(self):
237
+ self._top_level["main"] = WidgetDesc(
238
+ title="BEC Widgets", widget=BECDockArea(gui_id=self._gui_id)
239
+ )
208
240
  if self._auto_updates_enabled:
209
241
  if self._auto_updates is None:
210
242
  auto_updates = self._get_update_script()
211
243
  if auto_updates is None:
212
244
  AutoUpdates.create_default_dock = True
213
245
  AutoUpdates.enabled = True
214
- auto_updates = AutoUpdates(gui=self)
246
+ auto_updates = AutoUpdates(self._top_level["main"].widget)
215
247
  if auto_updates.create_default_dock:
216
248
  auto_updates.start_default_dock()
217
- # fig = auto_updates.get_default_figure()
249
+ self._start_update_script()
218
250
  self._auto_updates = auto_updates
251
+ self._do_show_all()
219
252
  self._gui_started_event.set()
220
- self.show_all()
221
253
 
222
254
  def start_server(self, wait=False) -> None:
223
255
  """
@@ -225,8 +257,8 @@ class BECGuiClientMixin:
225
257
  """
226
258
  if self._process is None or self._process.poll() is not None:
227
259
  logger.success("GUI starting...")
260
+ self._startup_timeout = 5
228
261
  self._gui_started_event.clear()
229
- self._start_update_script()
230
262
  self._process, self._process_output_processing_thread = _start_plot_process(
231
263
  self._gui_id, self.__class__, self._client._service_config.config, logger=logger
232
264
  )
@@ -239,27 +271,66 @@ class BECGuiClientMixin:
239
271
  threading.current_thread().cancel()
240
272
 
241
273
  self._gui_started_timer = RepeatTimer(
242
- 1, lambda: self.gui_is_alive() and gui_started_callback(self._gui_post_startup)
274
+ 0.5, lambda: self.gui_is_alive() and gui_started_callback(self._gui_post_startup)
243
275
  )
244
276
  self._gui_started_timer.start()
245
277
 
246
278
  if wait:
247
279
  self._gui_started_event.wait()
248
280
 
249
- def show_all(self):
250
- self._gui_started_event.wait()
281
+ def _dump(self):
282
+ rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
283
+ return rpc_client._run_rpc("_dump")
284
+
285
+ def start(self):
286
+ return self.start_server()
287
+
288
+ def _do_show_all(self):
251
289
  rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
252
290
  rpc_client._run_rpc("show")
291
+ for window in self._top_level.values():
292
+ window.widget.show()
293
+
294
+ def show_all(self):
295
+ with wait_for_server(self):
296
+ return self._do_show_all()
253
297
 
254
298
  def hide_all(self):
255
- self._gui_started_event.wait()
256
- rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
257
- rpc_client._run_rpc("hide")
299
+ with wait_for_server(self):
300
+ rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
301
+ rpc_client._run_rpc("hide")
302
+ for window in self._top_level.values():
303
+ window.widget.hide()
304
+
305
+ def show(self):
306
+ if self._process is not None:
307
+ return self.show_all()
308
+ # backward compatibility: show() was also starting server
309
+ return self.start_server(wait=True)
310
+
311
+ def hide(self):
312
+ return self.hide_all()
313
+
314
+ @property
315
+ def main(self):
316
+ """Return client to main dock area (in main window)"""
317
+ with wait_for_server(self):
318
+ return self._top_level["main"].widget
319
+
320
+ def new(self, title):
321
+ """Ask main window to create a new top-level dock area"""
322
+ with wait_for_server(self):
323
+ rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
324
+ widget = rpc_client._run_rpc("new_dock_area", title)
325
+ self._top_level[widget._gui_id] = WidgetDesc(title=title, widget=widget)
326
+ return widget
258
327
 
259
328
  def close(self) -> None:
260
329
  """
261
330
  Close the gui window.
262
331
  """
332
+ self._top_level.clear()
333
+
263
334
  if self._gui_started_timer is not None:
264
335
  self._gui_started_timer.cancel()
265
336
  self._gui_started_timer.join()
@@ -274,130 +345,3 @@ class BECGuiClientMixin:
274
345
  self._process_output_processing_thread.join()
275
346
  self._process.wait()
276
347
  self._process = None
277
- self.shutdown_auto_updates()
278
-
279
-
280
- class RPCResponseTimeoutError(Exception):
281
- """Exception raised when an RPC response is not received within the expected time."""
282
-
283
- def __init__(self, request_id, timeout):
284
- super().__init__(
285
- f"RPC response not received within {timeout} seconds for request ID {request_id}"
286
- )
287
-
288
-
289
- class RPCBase:
290
- def __init__(self, gui_id: str = None, config: dict = None, parent=None) -> None:
291
- self._client = BECClient() # BECClient is a singleton; here, we simply get the instance
292
- self._config = config if config is not None else {}
293
- self._gui_id = gui_id if gui_id is not None else str(uuid.uuid4())[:5]
294
- self._parent = parent
295
- self._msg_wait_event = threading.Event()
296
- self._rpc_response = None
297
- super().__init__()
298
- # print(f"RPCBase: {self._gui_id}")
299
-
300
- def __repr__(self):
301
- type_ = type(self)
302
- qualname = type_.__qualname__
303
- return f"<{qualname} object at {hex(id(self))}>"
304
-
305
- @property
306
- def _root(self):
307
- """
308
- Get the root widget. This is the BECFigure widget that holds
309
- the anchor gui_id.
310
- """
311
- parent = self
312
- # pylint: disable=protected-access
313
- while parent._parent is not None:
314
- parent = parent._parent
315
- return parent
316
-
317
- def _run_rpc(self, method, *args, wait_for_rpc_response=True, timeout=3, **kwargs):
318
- """
319
- Run the RPC call.
320
-
321
- Args:
322
- method: The method to call.
323
- args: The arguments to pass to the method.
324
- wait_for_rpc_response: Whether to wait for the RPC response.
325
- kwargs: The keyword arguments to pass to the method.
326
-
327
- Returns:
328
- The result of the RPC call.
329
- """
330
- request_id = str(uuid.uuid4())
331
- rpc_msg = messages.GUIInstructionMessage(
332
- action=method,
333
- parameter={"args": args, "kwargs": kwargs, "gui_id": self._gui_id},
334
- metadata={"request_id": request_id},
335
- )
336
-
337
- # pylint: disable=protected-access
338
- receiver = self._root._gui_id
339
- if wait_for_rpc_response:
340
- self._rpc_response = None
341
- self._msg_wait_event.clear()
342
- self._client.connector.register(
343
- MessageEndpoints.gui_instruction_response(request_id),
344
- cb=self._on_rpc_response,
345
- parent=self,
346
- )
347
-
348
- self._client.connector.set_and_publish(MessageEndpoints.gui_instructions(receiver), rpc_msg)
349
-
350
- if wait_for_rpc_response:
351
- try:
352
- finished = self._msg_wait_event.wait(10)
353
- if not finished:
354
- raise RPCResponseTimeoutError(request_id, timeout)
355
- finally:
356
- self._msg_wait_event.clear()
357
- self._client.connector.unregister(
358
- MessageEndpoints.gui_instruction_response(request_id), cb=self._on_rpc_response
359
- )
360
- # get class name
361
- if not self._rpc_response.accepted:
362
- raise ValueError(self._rpc_response.message["error"])
363
- msg_result = self._rpc_response.message.get("result")
364
- self._rpc_response = None
365
- return self._create_widget_from_msg_result(msg_result)
366
-
367
- @staticmethod
368
- def _on_rpc_response(msg: MessageObject, parent: RPCBase) -> None:
369
- msg = msg.value
370
- parent._msg_wait_event.set()
371
- parent._rpc_response = msg
372
-
373
- def _create_widget_from_msg_result(self, msg_result):
374
- if msg_result is None:
375
- return None
376
- if isinstance(msg_result, list):
377
- return [self._create_widget_from_msg_result(res) for res in msg_result]
378
- if isinstance(msg_result, dict):
379
- if "__rpc__" not in msg_result:
380
- return {
381
- key: self._create_widget_from_msg_result(val) for key, val in msg_result.items()
382
- }
383
- cls = msg_result.pop("widget_class", None)
384
- msg_result.pop("__rpc__", None)
385
-
386
- if not cls:
387
- return msg_result
388
-
389
- cls = getattr(client, cls)
390
- # print(msg_result)
391
- return cls(parent=self, **msg_result)
392
- return msg_result
393
-
394
- def gui_is_alive(self):
395
- """
396
- Check if the GUI is alive.
397
- """
398
- heart = self._client.connector.get(MessageEndpoints.gui_heartbeat(self._root._gui_id))
399
- if heart is None:
400
- return False
401
- if heart.status == messages.BECStatus.RUNNING:
402
- return True
403
- return False