bec-widgets 0.76.0__py3-none-any.whl → 0.77.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 (40) hide show
  1. CHANGELOG.md +42 -44
  2. PKG-INFO +2 -1
  3. bec_widgets/cli/client.py +73 -196
  4. bec_widgets/examples/jupyter_console/jupyter_console_window.py +25 -4
  5. bec_widgets/utils/bec_connector.py +66 -8
  6. bec_widgets/utils/colors.py +38 -0
  7. bec_widgets/utils/generate_designer_plugin.py +15 -14
  8. bec_widgets/utils/yaml_dialog.py +27 -3
  9. bec_widgets/widgets/console/console.py +496 -0
  10. bec_widgets/widgets/dock/dock.py +2 -2
  11. bec_widgets/widgets/dock/dock_area.py +2 -2
  12. bec_widgets/widgets/figure/figure.py +149 -195
  13. bec_widgets/widgets/figure/plots/image/image.py +62 -49
  14. bec_widgets/widgets/figure/plots/image/image_item.py +4 -3
  15. bec_widgets/widgets/figure/plots/motor_map/motor_map.py +98 -29
  16. bec_widgets/widgets/figure/plots/plot_base.py +1 -1
  17. bec_widgets/widgets/figure/plots/waveform/waveform.py +7 -8
  18. bec_widgets/widgets/figure/plots/waveform/waveform_curve.py +2 -2
  19. bec_widgets/widgets/ring_progress_bar/ring.py +3 -3
  20. bec_widgets/widgets/ring_progress_bar/ring_progress_bar.py +3 -3
  21. {bec_widgets-0.76.0.dist-info → bec_widgets-0.77.0.dist-info}/METADATA +2 -1
  22. {bec_widgets-0.76.0.dist-info → bec_widgets-0.77.0.dist-info}/RECORD +40 -38
  23. pyproject.toml +2 -1
  24. tests/end-2-end/test_bec_dock_rpc_e2e.py +16 -16
  25. tests/end-2-end/test_bec_figure_rpc_e2e.py +7 -7
  26. tests/end-2-end/test_rpc_register_e2e.py +8 -8
  27. tests/unit_tests/client_mocks.py +1 -0
  28. tests/unit_tests/test_bec_figure.py +49 -26
  29. tests/unit_tests/test_bec_motor_map.py +179 -41
  30. tests/unit_tests/test_color_validation.py +15 -0
  31. tests/unit_tests/test_device_input_base.py +1 -1
  32. tests/unit_tests/test_device_input_widgets.py +2 -0
  33. tests/unit_tests/test_generate_plugin.py +155 -0
  34. tests/unit_tests/test_motor_control.py +5 -4
  35. tests/unit_tests/test_plot_base.py +3 -3
  36. tests/unit_tests/test_waveform1d.py +18 -17
  37. tests/unit_tests/test_yaml_dialog.py +7 -7
  38. {bec_widgets-0.76.0.dist-info → bec_widgets-0.77.0.dist-info}/WHEEL +0 -0
  39. {bec_widgets-0.76.0.dist-info → bec_widgets-0.77.0.dist-info}/entry_points.txt +0 -0
  40. {bec_widgets-0.76.0.dist-info → bec_widgets-0.77.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,15 +1,19 @@
1
1
  # pylint: disable = no-name-in-module,missing-module-docstring
2
2
  from __future__ import annotations
3
3
 
4
+ import os
4
5
  import time
6
+ import uuid
5
7
  from typing import Optional
6
8
 
9
+ import yaml
7
10
  from bec_lib.utils.import_utils import lazy_import_from
8
11
  from pydantic import BaseModel, Field, field_validator
9
12
  from qtpy.QtCore import QObject, QRunnable, QThreadPool, Signal
10
13
  from qtpy.QtCore import Slot as pyqtSlot
11
14
 
12
15
  from bec_widgets.cli.rpc_register import RPCRegister
16
+ from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
13
17
 
14
18
  BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
15
19
 
@@ -62,7 +66,7 @@ class Worker(QRunnable):
62
66
  class BECConnector:
63
67
  """Connection mixin class for all BEC widgets, to handle BEC client and device manager"""
64
68
 
65
- USER_ACCESS = ["config_dict", "get_all_rpc"]
69
+ USER_ACCESS = ["_config_dict", "_get_all_rpc"]
66
70
 
67
71
  def __init__(self, client=None, config: ConnectionConfig = None, gui_id: str = None):
68
72
  # BEC related connections
@@ -126,23 +130,23 @@ class BECConnector:
126
130
  self._thread_pool.start(worker)
127
131
  return worker
128
132
 
129
- def get_all_rpc(self) -> dict:
133
+ def _get_all_rpc(self) -> dict:
130
134
  """Get all registered RPC objects."""
131
135
  all_connections = self.rpc_register.list_all_connections()
132
136
  return dict(all_connections)
133
137
 
134
138
  @property
135
- def rpc_id(self) -> str:
139
+ def _rpc_id(self) -> str:
136
140
  """Get the RPC ID of the widget."""
137
141
  return self.gui_id
138
142
 
139
- @rpc_id.setter
140
- def rpc_id(self, rpc_id: str) -> None:
143
+ @_rpc_id.setter
144
+ def _rpc_id(self, rpc_id: str) -> None:
141
145
  """Set the RPC ID of the widget."""
142
146
  self.gui_id = rpc_id
143
147
 
144
148
  @property
145
- def config_dict(self) -> dict:
149
+ def _config_dict(self) -> dict:
146
150
  """
147
151
  Get the configuration of the widget.
148
152
 
@@ -151,8 +155,8 @@ class BECConnector:
151
155
  """
152
156
  return self.config.model_dump()
153
157
 
154
- @config_dict.setter
155
- def config_dict(self, config: BaseModel) -> None:
158
+ @_config_dict.setter
159
+ def _config_dict(self, config: BaseModel) -> None:
156
160
  """
157
161
  Get the configuration of the widget.
158
162
 
@@ -161,6 +165,60 @@ class BECConnector:
161
165
  """
162
166
  self.config = config
163
167
 
168
+ def apply_config(self, config: dict, generate_new_id: bool = True) -> None:
169
+ """
170
+ Apply the configuration to the widget.
171
+
172
+ Args:
173
+ config(dict): Configuration settings.
174
+ generate_new_id(bool): If True, generate a new GUI ID for the widget.
175
+ """
176
+ self.config = ConnectionConfig(**config)
177
+ if generate_new_id is True:
178
+ gui_id = str(uuid.uuid4())
179
+ self.rpc_register.remove_rpc(self)
180
+ self.set_gui_id(gui_id)
181
+ self.rpc_register.add_rpc(self)
182
+ else:
183
+ self.gui_id = self.config.gui_id
184
+
185
+ def load_config(self, path: str | None = None, gui: bool = False):
186
+ """
187
+ Load the configuration of the widget from YAML.
188
+
189
+ Args:
190
+ path(str): Path to the configuration file for non-GUI dialog mode.
191
+ gui(bool): If True, use the GUI dialog to load the configuration file.
192
+ """
193
+ if gui is True:
194
+ config = load_yaml_gui(self)
195
+ else:
196
+ config = load_yaml(path)
197
+
198
+ if config is not None:
199
+ if config.get("widget_class") != self.__class__.__name__:
200
+ raise ValueError(
201
+ f"Configuration file is not for {self.__class__.__name__}. Got configuration for {config.get('widget_class')}."
202
+ )
203
+ self.apply_config(config)
204
+
205
+ def save_config(self, path: str | None = None, gui: bool = False):
206
+ """
207
+ Save the configuration of the widget to YAML.
208
+
209
+ Args:
210
+ path(str): Path to save the configuration file for non-GUI dialog mode.
211
+ gui(bool): If True, use the GUI dialog to save the configuration file.
212
+ """
213
+ if gui is True:
214
+ save_yaml_gui(self, self._config_dict)
215
+ else:
216
+ if path is None:
217
+ path = os.getcwd()
218
+ file_path = os.path.join(path, f"{self.__class__.__name__}_config.yaml")
219
+
220
+ save_yaml(file_path, self._config_dict)
221
+
164
222
  @pyqtSlot(str)
165
223
  def set_gui_id(self, gui_id: str) -> None:
166
224
  """
@@ -67,6 +67,44 @@ class Colors:
67
67
  raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.")
68
68
  return colors
69
69
 
70
+ @staticmethod
71
+ def hex_to_rgba(hex_color: str, alpha=255) -> tuple:
72
+ """
73
+ Convert HEX color to RGBA.
74
+
75
+ Args:
76
+ hex_color(str): HEX color string.
77
+ alpha(int): Alpha value (0-255). Default is 255 (opaque).
78
+
79
+ Returns:
80
+ tuple: RGBA color tuple (r, g, b, a).
81
+ """
82
+ hex_color = hex_color.lstrip("#")
83
+ if len(hex_color) == 6:
84
+ r, g, b = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
85
+ elif len(hex_color) == 8:
86
+ r, g, b, a = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4, 6))
87
+ return (r, g, b, a)
88
+ else:
89
+ raise ValueError("HEX color must be 6 or 8 characters long.")
90
+ return (r, g, b, alpha)
91
+
92
+ @staticmethod
93
+ def rgba_to_hex(r: int, g: int, b: int, a: int = 255) -> str:
94
+ """
95
+ Convert RGBA color to HEX.
96
+
97
+ Args:
98
+ r(int): Red value (0-255).
99
+ g(int): Green value (0-255).
100
+ b(int): Blue value (0-255).
101
+ a(int): Alpha value (0-255). Default is 255 (opaque).
102
+
103
+ Returns:
104
+ hec_color(str): HEX color string.
105
+ """
106
+ return "#{:02X}{:02X}{:02X}{:02X}".format(r, g, b, a)
107
+
70
108
  @staticmethod
71
109
  def validate_color(color: tuple | str) -> tuple | str:
72
110
  """
@@ -47,13 +47,12 @@ class DesignerPluginGenerator:
47
47
  def __init__(self, widget: type):
48
48
  self._excluded = False
49
49
  self.widget = widget
50
+ self.info = DesignerPluginInfo(widget)
50
51
  if widget.__name__ in EXCLUDED_PLUGINS:
51
52
 
52
53
  self._excluded = True
53
54
  return
54
55
 
55
- self.info = DesignerPluginInfo(widget)
56
-
57
56
  self.templates = {}
58
57
  self.template_path = os.path.join(
59
58
  os.path.dirname(os.path.abspath(__file__)), "plugin_templates"
@@ -75,7 +74,7 @@ class DesignerPluginGenerator:
75
74
 
76
75
  # Check if the widget class has parent as the first argument. This is a strict requirement of Qt!
77
76
  signature = list(inspect.signature(self.widget.__init__).parameters.values())
78
- if signature[1].name != "parent":
77
+ if len(signature) == 1 or signature[1].name != "parent":
79
78
  raise ValueError(
80
79
  f"Widget class {self.widget.__name__} must have parent as the first argument."
81
80
  )
@@ -89,20 +88,22 @@ class DesignerPluginGenerator:
89
88
  # Check if the widget class calls the super constructor with parent argument
90
89
  init_source = inspect.getsource(self.widget.__init__)
91
90
  cls_init_found = (
92
- bool(init_source.find(f"{base_cls[0].__name__}.__init__(self, parent=parent"))
93
- or bool(init_source.find(f"{base_cls[0].__name__}.__init__(self, parent)"))
94
- or bool(init_source.find(f"{base_cls[0].__name__}.__init__(self, parent,"))
91
+ bool(init_source.find(f"{base_cls[0].__name__}.__init__(self, parent=parent") > 0)
92
+ or bool(init_source.find(f"{base_cls[0].__name__}.__init__(self, parent)") > 0)
93
+ or bool(init_source.find(f"{base_cls[0].__name__}.__init__(self, parent,") > 0)
95
94
  )
96
95
  super_init_found = (
97
- bool(init_source.find(f"super({self.widget.__name__}, self).__init__(parent=parent"))
98
- or bool(init_source.find(f"super({self.widget.__name__}, self).__init__(parent,"))
99
- or bool(init_source.find(f"super({self.widget.__name__}, self).__init__(parent)"))
96
+ bool(
97
+ init_source.find(f"super({base_cls[0].__name__}, self).__init__(parent=parent") > 0
98
+ )
99
+ or bool(init_source.find(f"super({base_cls[0].__name__}, self).__init__(parent,") > 0)
100
+ or bool(init_source.find(f"super({base_cls[0].__name__}, self).__init__(parent)") > 0)
100
101
  )
101
- if issubclass(self.widget.__bases__[0], QObject) and super_init_found == -1:
102
+ if issubclass(self.widget.__bases__[0], QObject) and not super_init_found:
102
103
  super_init_found = (
103
- bool(init_source.find("super().__init__(parent=parent"))
104
- or bool(init_source.find("super().__init__(parent,"))
105
- or bool(init_source.find("super().__init__(parent)"))
104
+ bool(init_source.find("super().__init__(parent=parent") > 0)
105
+ or bool(init_source.find("super().__init__(parent,") > 0)
106
+ or bool(init_source.find("super().__init__(parent)") > 0)
106
107
  )
107
108
 
108
109
  if not cls_init_found and not super_init_found:
@@ -139,7 +140,7 @@ class DesignerPluginGenerator:
139
140
  self.templates[file.split(".")[0]] = f.read()
140
141
 
141
142
 
142
- if __name__ == "__main__":
143
+ if __name__ == "__main__": # pragma: no cover
143
144
  # from bec_widgets.widgets.bec_queue.bec_queue import BECQueue
144
145
  from bec_widgets.widgets.dock import BECDockArea
145
146
 
@@ -6,7 +6,7 @@ import yaml
6
6
  from qtpy.QtWidgets import QFileDialog
7
7
 
8
8
 
9
- def load_yaml(instance) -> Union[dict, None]:
9
+ def load_yaml_gui(instance) -> Union[dict, None]:
10
10
  """
11
11
  Load YAML file from disk.
12
12
 
@@ -20,12 +20,25 @@ def load_yaml(instance) -> Union[dict, None]:
20
20
  file_path, _ = QFileDialog.getOpenFileName(
21
21
  instance, "Load Settings", "", "YAML Files (*.yaml *.yml);;All Files (*)", options=options
22
22
  )
23
+ config = load_yaml(file_path)
24
+ return config
23
25
 
26
+
27
+ def load_yaml(file_path: str) -> Union[dict, None]:
28
+ """
29
+ Load YAML file from disk.
30
+
31
+ Args:
32
+ file_path(str): Path to the YAML file.
33
+
34
+ Returns:
35
+ dict: Configuration data loaded from the YAML file.
36
+ """
24
37
  if not file_path:
25
38
  return None
26
39
  try:
27
40
  with open(file_path, "r") as file:
28
- config = yaml.safe_load(file)
41
+ config = yaml.load(file, Loader=yaml.FullLoader)
29
42
  return config
30
43
 
31
44
  except FileNotFoundError:
@@ -38,7 +51,7 @@ def load_yaml(instance) -> Union[dict, None]:
38
51
  print(f"An error occurred while loading the settings from {file_path}: {e}")
39
52
 
40
53
 
41
- def save_yaml(instance, config: dict) -> None:
54
+ def save_yaml_gui(instance, config: dict) -> None:
42
55
  """
43
56
  Save YAML file to disk.
44
57
 
@@ -51,6 +64,17 @@ def save_yaml(instance, config: dict) -> None:
51
64
  instance, "Save Settings", "", "YAML Files (*.yaml *.yml);;All Files (*)", options=options
52
65
  )
53
66
 
67
+ save_yaml(file_path, config)
68
+
69
+
70
+ def save_yaml(file_path: str, config: dict) -> None:
71
+ """
72
+ Save YAML file to disk.
73
+
74
+ Args:
75
+ file_path(str): Path to the YAML file.
76
+ config(dict): Configuration data to be saved.
77
+ """
54
78
  if not file_path:
55
79
  return
56
80
  try: