bec-widgets 1.25.0__py3-none-any.whl → 2.0.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.
Files changed (197) hide show
  1. .gitlab-ci.yml +11 -6
  2. CHANGELOG.md +650 -0
  3. PKG-INFO +3 -3
  4. bec_widgets/__init__.py +4 -0
  5. bec_widgets/applications/bw_launch.py +23 -0
  6. bec_widgets/applications/launch_window.py +430 -0
  7. bec_widgets/assets/app_icons/auto_update.png +0 -0
  8. bec_widgets/assets/app_icons/ui_loader_tile.png +0 -0
  9. bec_widgets/cli/__init__.py +0 -1
  10. bec_widgets/cli/client.py +1779 -2064
  11. bec_widgets/cli/client_utils.py +346 -174
  12. bec_widgets/cli/generate_cli.py +143 -37
  13. bec_widgets/cli/rpc/rpc_base.py +152 -21
  14. bec_widgets/cli/rpc/rpc_register.py +113 -6
  15. bec_widgets/cli/rpc/rpc_widget_handler.py +13 -11
  16. bec_widgets/cli/server.py +125 -239
  17. bec_widgets/examples/jupyter_console/jupyter_console_window.py +97 -145
  18. bec_widgets/examples/plugin_example_pyside/tictactoetaskmenu.py +1 -1
  19. bec_widgets/utils/bec_connector.py +190 -21
  20. bec_widgets/utils/bec_designer.py +7 -0
  21. bec_widgets/utils/bec_dispatcher.py +71 -4
  22. bec_widgets/utils/bec_plugin_helper.py +89 -0
  23. bec_widgets/utils/bec_signal_proxy.py +1 -1
  24. bec_widgets/utils/bec_widget.py +26 -10
  25. bec_widgets/utils/colors.py +1 -1
  26. bec_widgets/{qt_utils → utils}/compact_popup.py +2 -0
  27. bec_widgets/utils/container_utils.py +37 -12
  28. bec_widgets/utils/crosshair.py +25 -8
  29. bec_widgets/utils/entry_validator.py +3 -1
  30. bec_widgets/{qt_utils → utils}/error_popups.py +18 -0
  31. bec_widgets/{qt_utils → utils}/expandable_frame.py +2 -2
  32. bec_widgets/utils/forms_from_types/forms.py +182 -0
  33. bec_widgets/{widgets/editors/scan_metadata/_metadata_widgets.py → utils/forms_from_types/items.py} +41 -30
  34. bec_widgets/utils/generate_designer_plugin.py +40 -36
  35. bec_widgets/utils/linear_region_selector.py +2 -0
  36. bec_widgets/utils/name_utils.py +16 -0
  37. bec_widgets/{qt_utils → utils}/palette_viewer.py +2 -2
  38. bec_widgets/utils/plot_indicator_items.py +2 -5
  39. bec_widgets/utils/plugin_utils.py +47 -1
  40. bec_widgets/{qt_utils → utils}/round_frame.py +14 -14
  41. bec_widgets/utils/rpc_server.py +277 -0
  42. bec_widgets/utils/serialization.py +44 -0
  43. bec_widgets/{qt_utils → utils}/settings_dialog.py +26 -1
  44. bec_widgets/{qt_utils → utils}/side_panel.py +17 -10
  45. bec_widgets/{qt_utils → utils}/toolbar.py +69 -25
  46. bec_widgets/utils/ui_loader.py +8 -8
  47. bec_widgets/utils/widget_io.py +166 -25
  48. bec_widgets/widgets/containers/auto_update/auto_updates.py +364 -0
  49. bec_widgets/widgets/containers/dock/dock.py +157 -49
  50. bec_widgets/widgets/containers/dock/dock_area.py +186 -138
  51. bec_widgets/widgets/containers/layout_manager/layout_manager.py +2 -1
  52. bec_widgets/widgets/containers/main_window/addons/web_links.py +15 -0
  53. bec_widgets/widgets/containers/main_window/main_window.py +189 -41
  54. bec_widgets/widgets/control/buttons/button_abort/button_abort.py +3 -4
  55. bec_widgets/widgets/control/buttons/button_reset/button_reset.py +3 -4
  56. bec_widgets/widgets/control/buttons/button_resume/button_resume.py +3 -3
  57. bec_widgets/widgets/control/buttons/stop_button/stop_button.py +18 -7
  58. bec_widgets/widgets/control/device_control/position_indicator/position_indicator.py +22 -3
  59. bec_widgets/widgets/control/device_control/positioner_box/_base/positioner_box_base.py +37 -18
  60. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.py +28 -4
  61. bec_widgets/widgets/control/device_control/positioner_box/positioner_box/positioner_box.ui +27 -4
  62. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.py +5 -2
  63. bec_widgets/widgets/control/device_control/positioner_box/positioner_box_2d/positioner_box_2d.ui +97 -31
  64. bec_widgets/widgets/control/device_control/positioner_box/positioner_control_line/positioner_control_line.ui +11 -4
  65. bec_widgets/widgets/control/device_control/positioner_group/positioner_group.py +2 -3
  66. bec_widgets/widgets/control/device_input/base_classes/device_input_base.py +29 -4
  67. bec_widgets/widgets/control/device_input/base_classes/device_signal_input_base.py +1 -0
  68. bec_widgets/widgets/control/device_input/device_combobox/device_combobox.py +2 -2
  69. bec_widgets/widgets/control/device_input/device_line_edit/device_line_edit.py +2 -2
  70. bec_widgets/widgets/control/device_input/signal_combobox/signal_combobox.py +1 -2
  71. bec_widgets/widgets/control/device_input/signal_line_edit/signal_line_edit.py +1 -2
  72. bec_widgets/widgets/control/scan_control/scan_control.py +7 -5
  73. bec_widgets/widgets/control/scan_control/scan_group_box.py +28 -5
  74. bec_widgets/widgets/dap/dap_combo_box/dap_combo_box.py +1 -2
  75. bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog.py +3 -4
  76. bec_widgets/widgets/dap/lmfit_dialog/lmfit_dialog_vertical.ui +14 -8
  77. bec_widgets/widgets/editors/console/console.py +1 -1
  78. bec_widgets/widgets/editors/{scan_metadata/additional_metadata_table.py → dict_backed_table.py} +29 -6
  79. bec_widgets/widgets/editors/scan_metadata/__init__.py +0 -7
  80. bec_widgets/widgets/editors/scan_metadata/_util.py +1 -1
  81. bec_widgets/widgets/{plots/motor_map/register_bec_motor_map_widget.py → editors/scan_metadata/register_scan_metadata.py} +2 -4
  82. bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +42 -136
  83. bec_widgets/widgets/editors/scan_metadata/scan_metadata.pyproject +1 -0
  84. bec_widgets/widgets/{plots/multi_waveform/bec_multi_waveform_widget_plugin.py → editors/scan_metadata/scan_metadata_plugin.py} +9 -9
  85. bec_widgets/widgets/editors/text_box/text_box.py +2 -3
  86. bec_widgets/widgets/editors/website/website.py +2 -2
  87. bec_widgets/widgets/games/minesweeper.py +3 -2
  88. bec_widgets/widgets/plots/image/image.py +960 -0
  89. bec_widgets/widgets/plots/image/image.pyproject +1 -0
  90. bec_widgets/widgets/plots/image/image_item.py +279 -0
  91. bec_widgets/widgets/plots/{motor_map/bec_motor_map_widget_plugin.py → image/image_plugin.py} +11 -13
  92. bec_widgets/widgets/{containers/figure/plots → plots}/image/image_processor.py +31 -64
  93. bec_widgets/widgets/plots/image/{register_bec_image_widget.py → register_image.py} +2 -2
  94. bec_widgets/widgets/plots/image/toolbar_bundles/image_selection.py +59 -0
  95. bec_widgets/widgets/plots/image/toolbar_bundles/processing.py +79 -0
  96. bec_widgets/widgets/plots/motor_map/motor_map.py +832 -0
  97. bec_widgets/widgets/plots/motor_map/motor_map.pyproject +1 -0
  98. bec_widgets/widgets/plots/motor_map/motor_map_plugin.py +54 -0
  99. bec_widgets/widgets/plots/{multi_waveform/register_bec_multi_waveform_widget.py → motor_map/register_motor_map.py} +2 -4
  100. bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.py +129 -0
  101. bec_widgets/widgets/plots/motor_map/settings/motor_map_settings.ui +120 -0
  102. bec_widgets/widgets/plots/motor_map/toolbar_bundles/motor_selection.py +70 -0
  103. bec_widgets/widgets/plots/multi_waveform/multi_waveform.py +508 -0
  104. bec_widgets/widgets/plots/multi_waveform/multi_waveform.pyproject +1 -0
  105. bec_widgets/widgets/plots/multi_waveform/multi_waveform_plugin.py +54 -0
  106. bec_widgets/widgets/plots/multi_waveform/register_multi_waveform.py +15 -0
  107. bec_widgets/widgets/plots/multi_waveform/settings/control_panel.py +144 -0
  108. bec_widgets/widgets/plots/multi_waveform/settings/multi_waveform_controls.ui +164 -0
  109. bec_widgets/widgets/plots/multi_waveform/toolbar_bundles/monitor_selection.py +65 -0
  110. bec_widgets/widgets/{plots_next_gen → plots}/plot_base.py +321 -40
  111. bec_widgets/widgets/plots/{waveform/register_bec_waveform_widget.py → scatter_waveform/register_scatter_waveform.py} +3 -3
  112. bec_widgets/widgets/plots/scatter_waveform/scatter_curve.py +197 -0
  113. bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.py +553 -0
  114. bec_widgets/widgets/plots/scatter_waveform/scatter_waveform.pyproject +1 -0
  115. bec_widgets/widgets/plots/{image/bec_image_widget_plugin.py → scatter_waveform/scatter_waveform_plugin.py} +9 -13
  116. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_setting.py +138 -0
  117. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_horizontal.ui +195 -0
  118. bec_widgets/widgets/plots/scatter_waveform/settings/scatter_curve_settings_vertical.ui +204 -0
  119. bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings.py +8 -8
  120. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/mouse_interactions.py +4 -18
  121. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/plot_export.py +14 -3
  122. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/roi_bundle.py +6 -1
  123. bec_widgets/widgets/{plots_next_gen → plots}/toolbar_bundles/save_state.py +2 -2
  124. bec_widgets/widgets/{containers/figure/plots/waveform/waveform_curve.py → plots/waveform/curve.py} +119 -49
  125. bec_widgets/widgets/plots/waveform/register_waveform.py +15 -0
  126. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_setting.py +125 -0
  127. bec_widgets/widgets/plots/waveform/settings/curve_settings/curve_tree.py +576 -0
  128. bec_widgets/widgets/plots/waveform/utils/__init__.py +0 -0
  129. bec_widgets/widgets/plots/waveform/utils/roi_manager.py +84 -0
  130. bec_widgets/widgets/plots/waveform/waveform.py +1794 -0
  131. bec_widgets/widgets/plots/waveform/waveform.pyproject +1 -0
  132. bec_widgets/widgets/plots/waveform/{bec_waveform_widget_plugin.py → waveform_plugin.py} +9 -13
  133. bec_widgets/widgets/progress/bec_progressbar/bec_progressbar.py +1 -2
  134. bec_widgets/widgets/progress/ring_progress_bar/ring.py +11 -10
  135. bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py +24 -14
  136. bec_widgets/widgets/services/bec_queue/bec_queue.py +13 -11
  137. bec_widgets/widgets/services/bec_status_box/bec_status_box.py +3 -4
  138. bec_widgets/widgets/services/device_browser/device_browser.py +5 -2
  139. bec_widgets/widgets/services/device_browser/device_item/device_item.py +1 -1
  140. bec_widgets/widgets/utility/logpanel/logpanel.py +36 -17
  141. bec_widgets/widgets/utility/spinbox/decimal_spinbox.py +3 -3
  142. bec_widgets/widgets/utility/spinner/spinner.py +2 -2
  143. bec_widgets/widgets/utility/visual/color_button/color_button.py +1 -1
  144. bec_widgets/widgets/utility/visual/colormap_widget/colormap_widget.py +4 -6
  145. bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py +4 -8
  146. {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/METADATA +3 -3
  147. {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/RECORD +169 -154
  148. pyproject.toml +3 -3
  149. bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +0 -198
  150. bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +0 -615
  151. bec_widgets/applications/bec_app.py +0 -84
  152. bec_widgets/cli/auto_updates.py +0 -168
  153. bec_widgets/widgets/containers/figure/__init__.py +0 -1
  154. bec_widgets/widgets/containers/figure/figure.py +0 -796
  155. bec_widgets/widgets/containers/figure/plots/axis_settings.py +0 -91
  156. bec_widgets/widgets/containers/figure/plots/axis_settings.ui +0 -256
  157. bec_widgets/widgets/containers/figure/plots/image/image.py +0 -772
  158. bec_widgets/widgets/containers/figure/plots/image/image_item.py +0 -337
  159. bec_widgets/widgets/containers/figure/plots/motor_map/motor_map.py +0 -525
  160. bec_widgets/widgets/containers/figure/plots/multi_waveform/multi_waveform.py +0 -340
  161. bec_widgets/widgets/containers/figure/plots/plot_base.py +0 -505
  162. bec_widgets/widgets/containers/figure/plots/waveform/waveform.py +0 -1563
  163. bec_widgets/widgets/plots/image/bec_image_widget.pyproject +0 -1
  164. bec_widgets/widgets/plots/image/image_widget.py +0 -515
  165. bec_widgets/widgets/plots/motor_map/bec_motor_map_widget.pyproject +0 -1
  166. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.py +0 -56
  167. bec_widgets/widgets/plots/motor_map/motor_map_dialog/motor_map_settings.ui +0 -108
  168. bec_widgets/widgets/plots/motor_map/motor_map_widget.py +0 -234
  169. bec_widgets/widgets/plots/multi_waveform/bec_multi_waveform_widget.pyproject +0 -1
  170. bec_widgets/widgets/plots/multi_waveform/multi_waveform_controls.ui +0 -99
  171. bec_widgets/widgets/plots/multi_waveform/multi_waveform_widget.py +0 -536
  172. bec_widgets/widgets/plots/waveform/bec_waveform_widget.pyproject +0 -1
  173. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.py +0 -336
  174. bec_widgets/widgets/plots/waveform/waveform_popups/curve_dialog/curve_dialog.ui +0 -372
  175. bec_widgets/widgets/plots/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py +0 -25
  176. bec_widgets/widgets/plots/waveform/waveform_widget.py +0 -751
  177. /bec_widgets/{qt_utils → utils}/collapsible_panel_manager.py +0 -0
  178. /bec_widgets/{applications/alignment → utils/forms_from_types}/__init__.py +0 -0
  179. /bec_widgets/{qt_utils → utils}/redis_message_waiter.py +0 -0
  180. /bec_widgets/{applications/alignment/alignment_1d → widgets/containers/auto_update}/__init__.py +0 -0
  181. /bec_widgets/{qt_utils → widgets/containers/main_window/addons}/__init__.py +0 -0
  182. /bec_widgets/widgets/{containers/figure/plots → plots/image/toolbar_bundles}/__init__.py +0 -0
  183. /bec_widgets/widgets/{containers/figure/plots/image → plots/motor_map/settings}/__init__.py +0 -0
  184. /bec_widgets/widgets/{containers/figure/plots/motor_map → plots/motor_map/toolbar_bundles}/__init__.py +0 -0
  185. /bec_widgets/widgets/{containers/figure/plots/multi_waveform → plots/multi_waveform/settings}/__init__.py +0 -0
  186. /bec_widgets/widgets/{containers/figure/plots/waveform → plots/multi_waveform/toolbar_bundles}/__init__.py +0 -0
  187. /bec_widgets/widgets/plots/{motor_map/motor_map_dialog → scatter_waveform}/__init__.py +0 -0
  188. /bec_widgets/widgets/plots/{waveform/waveform_popups → scatter_waveform/settings}/__init__.py +0 -0
  189. /bec_widgets/widgets/plots/{waveform/waveform_popups/curve_dialog → setting_menus}/__init__.py +0 -0
  190. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_horizontal.ui +0 -0
  191. /bec_widgets/widgets/{plots_next_gen → plots}/setting_menus/axis_settings_vertical.ui +0 -0
  192. /bec_widgets/widgets/plots/{waveform/waveform_popups/dap_summary_dialog → toolbar_bundles}/__init__.py +0 -0
  193. /bec_widgets/widgets/{plots_next_gen/setting_menus → plots/waveform/settings}/__init__.py +0 -0
  194. /bec_widgets/widgets/{plots_next_gen/toolbar_bundles → plots/waveform/settings/curve_settings}/__init__.py +0 -0
  195. {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/WHEEL +0 -0
  196. {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/entry_points.txt +0 -0
  197. {bec_widgets-1.25.0.dist-info → bec_widgets-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,17 +2,22 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import argparse
5
+ import importlib
5
6
  import inspect
6
7
  import os
7
8
  import sys
9
+ from pathlib import Path
8
10
 
9
11
  import black
10
12
  import isort
13
+ from bec_lib.logger import bec_logger
11
14
  from qtpy.QtCore import Property as QtProperty
12
15
 
13
- from bec_widgets.utils.generate_designer_plugin import DesignerPluginGenerator
16
+ from bec_widgets.utils.generate_designer_plugin import DesignerPluginGenerator, plugin_filenames
14
17
  from bec_widgets.utils.plugin_utils import BECClassContainer, get_custom_classes
15
18
 
19
+ logger = bec_logger.logger
20
+
16
21
  if sys.version_info >= (3, 11):
17
22
  from typing import get_overloads
18
23
  else:
@@ -29,13 +34,28 @@ else:
29
34
 
30
35
 
31
36
  class ClientGenerator:
32
- def __init__(self):
33
- self.header = """# This file was automatically generated by generate_cli.py\n
37
+ def __init__(self, base=False):
38
+ self._base = base
39
+ base_imports = (
40
+ """import enum
41
+ import inspect
42
+ import traceback
43
+ from functools import reduce
44
+ from typing import Literal, Optional
45
+ """
46
+ if self._base
47
+ else "\n"
48
+ )
49
+ self.header = f"""# This file was automatically generated by generate_cli.py
50
+ # type: ignore \n
34
51
  from __future__ import annotations
35
- import enum
36
- from typing import Literal, Optional, overload
52
+ {base_imports}
53
+ from bec_lib.logger import bec_logger
37
54
 
38
55
  from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call
56
+ {"from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets, get_plugin_client_module" if self._base else ""}
57
+
58
+ logger = bec_logger.logger
39
59
 
40
60
  # pylint: skip-file"""
41
61
 
@@ -62,6 +82,7 @@ from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call
62
82
 
63
83
  self.write_client_enum(rpc_top_level_classes)
64
84
  for cls in connector_classes:
85
+ logger.debug(f"generating RPC client class for {cls.__name__}")
65
86
  self.content += "\n\n"
66
87
  self.generate_content_for_class(cls)
67
88
 
@@ -69,14 +90,50 @@ from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call
69
90
  """
70
91
  Write the client enum to the content.
71
92
  """
93
+ if self._base:
94
+ self.content += """
95
+ class _WidgetsEnumType(str, enum.Enum):
96
+ \"\"\" Enum for the available widgets, to be generated programatically \"\"\"
97
+ ...
98
+ """
99
+
72
100
  self.content += """
73
- class Widgets(str, enum.Enum):
74
- \"\"\"
75
- Enum for the available widgets.
76
- \"\"\"
101
+
102
+ _Widgets = {
77
103
  """
78
104
  for cls in published_classes:
79
- self.content += f'{cls.__name__} = "{cls.__name__}"\n '
105
+ self.content += f'"{cls.__name__}": "{cls.__name__}",\n '
106
+
107
+ self.content += """}
108
+ """
109
+ if self._base:
110
+ self.content += """
111
+
112
+ try:
113
+ _plugin_widgets = get_all_plugin_widgets()
114
+ plugin_client = get_plugin_client_module()
115
+ Widgets = _WidgetsEnumType("Widgets", {name: name for name in _plugin_widgets} | _Widgets)
116
+
117
+ if (_overlap := _Widgets.keys() & _plugin_widgets.keys()) != set():
118
+ for _widget in _overlap:
119
+ logger.warning(f"Detected duplicate widget {_widget} in plugin repo file: {inspect.getfile(_plugin_widgets[_widget])} !")
120
+ for plugin_name, plugin_class in inspect.getmembers(plugin_client, inspect.isclass):
121
+ if issubclass(plugin_class, RPCBase) and plugin_class is not RPCBase:
122
+ if plugin_name in globals():
123
+ conflicting_file = (
124
+ inspect.getfile(_plugin_widgets[plugin_name])
125
+ if plugin_name in _plugin_widgets
126
+ else f"{plugin_client}"
127
+ )
128
+ logger.warning(
129
+ f"Plugin widget {plugin_name} from {conflicting_file} conflicts with a built-in class!"
130
+ )
131
+ continue
132
+ if plugin_name not in _overlap:
133
+ globals()[plugin_name] = plugin_class
134
+ except ImportError as e:
135
+ logger.error(f"Failed loading plugins: \\n{reduce(add, traceback.format_exception(e))}")
136
+ """
80
137
 
81
138
  def generate_content_for_class(self, cls):
82
139
  """
@@ -95,9 +152,21 @@ class {class_name}(RPCBase):"""
95
152
  self.content += f"""
96
153
  class {class_name}(RPCBase):"""
97
154
 
155
+ if cls.__doc__:
156
+ # We only want the first line of the docstring
157
+ # But skip the first line if it's a blank line
158
+ first_line = cls.__doc__.split("\n")[0]
159
+ if first_line:
160
+ class_docs = first_line
161
+ else:
162
+ class_docs = cls.__doc__.split("\n")[1]
163
+ self.content += f"""
164
+ \"\"\"{class_docs}\"\"\"
165
+ """
98
166
  if not cls.USER_ACCESS:
99
167
  self.content += """...
100
168
  """
169
+
101
170
  for method in cls.USER_ACCESS:
102
171
  is_property_setter = False
103
172
  obj = getattr(cls, method, None)
@@ -176,41 +245,78 @@ def main():
176
245
  """
177
246
 
178
247
  parser = argparse.ArgumentParser(description="Auto-generate the client for RPC widgets")
179
- parser.add_argument("--core", action="store_true", help="Whether to generate the core client")
248
+ parser.add_argument(
249
+ "--target",
250
+ action="store",
251
+ type=str,
252
+ help="Which package to generate plugin files for. Should be installed in the local environment (example: my_plugin_repo)",
253
+ )
180
254
 
181
255
  args = parser.parse_args()
256
+ if args.target is None:
257
+ logger.error(
258
+ "You must provide a target - for safety, the default of running this on bec_widgets core has been removed. To generate the client for bec_widgets, run `bw-generate-cli --target bec_widgets`"
259
+ )
260
+ return
182
261
 
183
- if args.core:
184
- current_path = os.path.dirname(__file__)
185
- client_path = os.path.join(current_path, "client.py")
262
+ logger.info(f"BEC Widget code generation tool started with args: {args}")
186
263
 
187
- rpc_classes = get_custom_classes("bec_widgets")
264
+ client_subdir = "cli" if args.target == "bec_widgets" else "widgets"
265
+ module_name = "bec_widgets" if args.target == "bec_widgets" else f"{args.target}.bec_widgets"
188
266
 
189
- generator = ClientGenerator()
190
- generator.generate_client(rpc_classes)
191
- generator.write(client_path)
267
+ try:
268
+ module = importlib.import_module(module_name)
269
+ assert module.__file__ is not None
270
+ module_file = Path(module.__file__)
271
+ module_dir = module_file.parent if module_file.is_file() else module_file
272
+ except Exception as e:
273
+ logger.error(f"Failed to load module {module_name} for code generation: {e}")
274
+ return
192
275
 
193
- for cls in rpc_classes.plugins:
194
- plugin = DesignerPluginGenerator(cls)
195
- if not hasattr(plugin, "info"):
196
- continue
276
+ client_path = module_dir / client_subdir / "client.py"
197
277
 
198
- # if the class directory already has a register, plugin and pyproject file, skip
199
- if os.path.exists(
200
- os.path.join(plugin.info.base_path, f"register_{plugin.info.plugin_name_snake}.py")
201
- ):
202
- continue
203
- if os.path.exists(
204
- os.path.join(plugin.info.base_path, f"{plugin.info.plugin_name_snake}_plugin.py")
205
- ):
206
- continue
207
- if os.path.exists(
208
- os.path.join(plugin.info.base_path, f"{plugin.info.plugin_name_snake}.pyproject")
209
- ):
210
- continue
211
- plugin.run()
278
+ rpc_classes = get_custom_classes(module_name)
279
+ logger.info(f"Obtained classes with RPC objects: {rpc_classes!r}")
280
+
281
+ generator = ClientGenerator(base=module_name == "bec_widgets")
282
+ logger.info(f"Generating client file at {client_path}")
283
+ generator.generate_client(rpc_classes)
284
+ generator.write(str(client_path))
285
+
286
+ if module_name != "bec_widgets":
287
+ non_overwrite_classes = list(clsinfo.name for clsinfo in get_custom_classes("bec_widgets"))
288
+ logger.info(
289
+ f"Not writing plugins which would conflict with builtin classes: {non_overwrite_classes}"
290
+ )
291
+ else:
292
+ non_overwrite_classes = []
293
+
294
+ for cls in rpc_classes.plugins:
295
+ logger.info(f"Writing bec-designer plugin files for {cls.__name__}...")
296
+
297
+ if cls.__name__ in non_overwrite_classes:
298
+ logger.error(
299
+ f"Not writing plugin files for {cls.__name__} because a built-in widget with that name exists"
300
+ )
301
+
302
+ plugin = DesignerPluginGenerator(cls)
303
+ if not hasattr(plugin, "info"):
304
+ continue
305
+
306
+ def _exists(file: str):
307
+ return os.path.exists(os.path.join(plugin.info.base_path, file))
308
+
309
+ if any(_exists(file) for file in plugin_filenames(plugin.info.plugin_name_snake)):
310
+ logger.debug(
311
+ f"Skipping generation of extra plugin files for {plugin.info.plugin_name_snake} - at least one file out of 'plugin.py', 'pyproject', and 'register_{plugin.info.plugin_name_snake}.py' already exists."
312
+ )
313
+ continue
314
+
315
+ plugin.run()
212
316
 
213
317
 
214
318
  if __name__ == "__main__": # pragma: no cover
215
- sys.argv = ["generate_cli.py", "--core"]
319
+ import sys
320
+
321
+ sys.argv = ["bw-generate-cli", "--target", "csaxs_bec"]
216
322
  main()
@@ -1,24 +1,28 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import inspect
3
4
  import threading
4
5
  import uuid
5
6
  from functools import wraps
6
- from typing import TYPE_CHECKING
7
+ from typing import TYPE_CHECKING, Any, cast
7
8
 
8
9
  from bec_lib.client import BECClient
9
10
  from bec_lib.endpoints import MessageEndpoints
10
11
  from bec_lib.utils.import_utils import lazy_import, lazy_import_from
11
12
 
12
- import bec_widgets.cli.client as client
13
-
14
- if TYPE_CHECKING:
13
+ if TYPE_CHECKING: # pragma: no cover
15
14
  from bec_lib import messages
16
15
  from bec_lib.connector import MessageObject
16
+
17
+ import bec_widgets.cli.client as client
18
+ from bec_widgets.cli.client_utils import BECGuiClient
17
19
  else:
20
+ client = lazy_import("bec_widgets.cli.client") # avoid circular import
18
21
  messages = lazy_import("bec_lib.messages")
19
- # from bec_lib.connector import MessageObject
20
22
  MessageObject = lazy_import_from("bec_lib.connector", ("MessageObject",))
21
23
 
24
+ # pylint: disable=protected-access
25
+
22
26
 
23
27
  def rpc_call(func):
24
28
  """
@@ -35,6 +39,14 @@ def rpc_call(func):
35
39
  def wrapper(self, *args, **kwargs):
36
40
  # we could rely on a strict type check here, but this is more flexible
37
41
  # moreover, it would anyway crash for objects...
42
+ caller_frame = inspect.currentframe().f_back # type: ignore
43
+ while caller_frame:
44
+ if "jedi" in caller_frame.f_globals:
45
+ # Jedi module is present, likely tab completion
46
+ # Do not run the RPC call
47
+ return None # func(*args, **kwargs)
48
+ caller_frame = caller_frame.f_back
49
+
38
50
  out = []
39
51
  for arg in args:
40
52
  if hasattr(arg, "name"):
@@ -44,7 +56,7 @@ def rpc_call(func):
44
56
  for key, val in kwargs.items():
45
57
  if hasattr(val, "name"):
46
58
  kwargs[key] = val.name
47
- if not self.gui_is_alive():
59
+ if not self._root._gui_is_alive():
48
60
  raise RuntimeError("GUI is not alive")
49
61
  return self._run_rpc(func.__name__, *args, **kwargs)
50
62
 
@@ -60,24 +72,96 @@ class RPCResponseTimeoutError(Exception):
60
72
  )
61
73
 
62
74
 
75
+ class DeletedWidgetError(Exception): ...
76
+
77
+
78
+ def check_for_deleted_widget(func):
79
+ @wraps(func)
80
+ def wrapper(self, *args, **kwargs):
81
+ if self._gui_id not in self._registry:
82
+ raise DeletedWidgetError(f"Widget with gui_id {self._gui_id} has been deleted")
83
+ return func(self, *args, **kwargs)
84
+
85
+ return wrapper
86
+
87
+
88
+ class RPCReference:
89
+ def __init__(self, registry: dict, gui_id: str) -> None:
90
+ self._registry = registry
91
+ self._gui_id = gui_id
92
+ self.object_name = self._registry[self._gui_id].object_name
93
+
94
+ @check_for_deleted_widget
95
+ def __getattr__(self, name):
96
+ if name in ["_registry", "_gui_id", "_is_deleted", "object_name"]:
97
+ return super().__getattribute__(name)
98
+ return self._registry[self._gui_id].__getattribute__(name)
99
+
100
+ def __setattr__(self, name, value):
101
+ if name in ["_registry", "_gui_id", "_is_deleted", "object_name"]:
102
+ return super().__setattr__(name, value)
103
+ if self._gui_id not in self._registry:
104
+ raise DeletedWidgetError(f"Widget with gui_id {self._gui_id} has been deleted")
105
+ self._registry[self._gui_id].__setattr__(name, value)
106
+
107
+ def __repr__(self):
108
+ if self._gui_id not in self._registry:
109
+ return f"<Deleted widget with gui_id {self._gui_id}>"
110
+ return self._registry[self._gui_id].__repr__()
111
+
112
+ def __str__(self):
113
+ if self._gui_id not in self._registry:
114
+ return f"<Deleted widget with gui_id {self._gui_id}>"
115
+ return self._registry[self._gui_id].__str__()
116
+
117
+ def __dir__(self):
118
+ if self._gui_id not in self._registry:
119
+ return []
120
+ return self._registry[self._gui_id].__dir__()
121
+
122
+ def _is_deleted(self) -> bool:
123
+ return self._gui_id not in self._registry
124
+
125
+
63
126
  class RPCBase:
64
- def __init__(self, gui_id: str = None, config: dict = None, parent=None) -> None:
127
+ def __init__(
128
+ self,
129
+ gui_id: str | None = None,
130
+ config: dict | None = None,
131
+ object_name: str | None = None,
132
+ parent=None,
133
+ **kwargs,
134
+ ) -> None:
65
135
  self._client = BECClient() # BECClient is a singleton; here, we simply get the instance
66
136
  self._config = config if config is not None else {}
67
137
  self._gui_id = gui_id if gui_id is not None else str(uuid.uuid4())[:5]
138
+ self.object_name = object_name if object_name is not None else str(uuid.uuid4())[:5]
68
139
  self._parent = parent
69
140
  self._msg_wait_event = threading.Event()
70
141
  self._rpc_response = None
71
142
  super().__init__()
72
- # print(f"RPCBase: {self._gui_id}")
143
+ self._rpc_references: dict[str, str] = {}
73
144
 
74
145
  def __repr__(self):
75
146
  type_ = type(self)
76
147
  qualname = type_.__qualname__
77
- return f"<{qualname} object at {hex(id(self))}>"
148
+ return f"<{qualname} with name: {self.object_name}>"
149
+
150
+ def remove(self):
151
+ """
152
+ Remove the widget.
153
+ """
154
+ obj = self._root._server_registry.get(self._gui_id)
155
+ if obj is None:
156
+ raise ValueError(f"Widget {self._gui_id} not found.")
157
+ if proxy := obj.get("container_proxy"):
158
+ assert isinstance(proxy, str)
159
+ self._run_rpc("remove", gui_id=proxy)
160
+ return
161
+ self._run_rpc("remove")
78
162
 
79
163
  @property
80
- def _root(self):
164
+ def _root(self) -> BECGuiClient:
81
165
  """
82
166
  Get the root widget. This is the BECFigure widget that holds
83
167
  the anchor gui_id.
@@ -86,9 +170,17 @@ class RPCBase:
86
170
  # pylint: disable=protected-access
87
171
  while parent._parent is not None:
88
172
  parent = parent._parent
89
- return parent
173
+ return parent # type: ignore
90
174
 
91
- def _run_rpc(self, method, *args, wait_for_rpc_response=True, timeout=3, **kwargs):
175
+ def _run_rpc(
176
+ self,
177
+ method,
178
+ *args,
179
+ wait_for_rpc_response=True,
180
+ timeout=5,
181
+ gui_id: str | None = None,
182
+ **kwargs,
183
+ ) -> Any:
92
184
  """
93
185
  Run the RPC call.
94
186
 
@@ -96,6 +188,8 @@ class RPCBase:
96
188
  method: The method to call.
97
189
  args: The arguments to pass to the method.
98
190
  wait_for_rpc_response: Whether to wait for the RPC response.
191
+ timeout: The timeout for the RPC response.
192
+ gui_id: The GUI ID to use for the RPC call. If None, the default GUI ID is used.
99
193
  kwargs: The keyword arguments to pass to the method.
100
194
 
101
195
  Returns:
@@ -104,10 +198,9 @@ class RPCBase:
104
198
  request_id = str(uuid.uuid4())
105
199
  rpc_msg = messages.GUIInstructionMessage(
106
200
  action=method,
107
- parameter={"args": args, "kwargs": kwargs, "gui_id": self._gui_id},
201
+ parameter={"args": args, "kwargs": kwargs, "gui_id": gui_id or self._gui_id},
108
202
  metadata={"request_id": request_id},
109
203
  )
110
-
111
204
  # pylint: disable=protected-access
112
205
  receiver = self._root._gui_id
113
206
  if wait_for_rpc_response:
@@ -131,7 +224,11 @@ class RPCBase:
131
224
  self._client.connector.unregister(
132
225
  MessageEndpoints.gui_instruction_response(request_id), cb=self._on_rpc_response
133
226
  )
134
- # get class name
227
+
228
+ # we can assume that the response is a RequestResponseMessage, updated by
229
+ # the _on_rpc_response method
230
+ assert isinstance(self._rpc_response, messages.RequestResponseMessage)
231
+
135
232
  if not self._rpc_response.accepted:
136
233
  raise ValueError(self._rpc_response.message["error"])
137
234
  msg_result = self._rpc_response.message.get("result")
@@ -139,10 +236,10 @@ class RPCBase:
139
236
  return self._create_widget_from_msg_result(msg_result)
140
237
 
141
238
  @staticmethod
142
- def _on_rpc_response(msg: MessageObject, parent: RPCBase) -> None:
143
- msg = msg.value
144
- parent._msg_wait_event.set()
239
+ def _on_rpc_response(msg_obj: MessageObject, parent: RPCBase) -> None:
240
+ msg = cast(messages.RequestResponseMessage, msg_obj.value)
145
241
  parent._rpc_response = msg
242
+ parent._msg_wait_event.set()
146
243
 
147
244
  def _create_widget_from_msg_result(self, msg_result):
148
245
  if msg_result is None:
@@ -161,11 +258,21 @@ class RPCBase:
161
258
  return msg_result
162
259
 
163
260
  cls = getattr(client, cls)
164
- # print(msg_result)
165
- return cls(parent=self, **msg_result)
261
+ # The namespace of the object will be updated dynamically on the client side
262
+ # Therefore it is important to check if the object is already in the registry
263
+ # If yes, we return the reference to the object, otherwise we create a new object
264
+ # pylint: disable=protected-access
265
+ if msg_result["gui_id"] in self._root._ipython_registry:
266
+ return RPCReference(self._root._ipython_registry, msg_result["gui_id"])
267
+ ret = cls(parent=self, **msg_result)
268
+ self._root._ipython_registry[ret._gui_id] = ret
269
+ self._refresh_references()
270
+ obj = RPCReference(self._root._ipython_registry, ret._gui_id)
271
+ return obj
272
+ # return ret
166
273
  return msg_result
167
274
 
168
- def gui_is_alive(self):
275
+ def _gui_is_alive(self):
169
276
  """
170
277
  Check if the GUI is alive.
171
278
  """
@@ -175,3 +282,27 @@ class RPCBase:
175
282
  if heart.status == messages.BECStatus.RUNNING:
176
283
  return True
177
284
  return False
285
+
286
+ def _refresh_references(self):
287
+ """
288
+ Refresh the references.
289
+ """
290
+ with self._root._lock:
291
+ references = {}
292
+ for key, val in self._root._server_registry.items():
293
+ parent_id = val["config"].get("parent_id")
294
+ if parent_id == self._gui_id:
295
+ references[key] = {
296
+ "gui_id": val["config"]["gui_id"],
297
+ "object_name": val["object_name"],
298
+ }
299
+ removed_references = set(self._rpc_references.keys()) - set(references.keys())
300
+ for key in removed_references:
301
+ delattr(self, self._rpc_references[key]["object_name"])
302
+ self._rpc_references = references
303
+ for key, val in references.items():
304
+ setattr(
305
+ self,
306
+ val["object_name"],
307
+ RPCReference(self._root._ipython_registry, val["gui_id"]),
308
+ )