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.
- CHANGELOG.md +42 -44
- PKG-INFO +2 -1
- bec_widgets/cli/client.py +73 -196
- bec_widgets/examples/jupyter_console/jupyter_console_window.py +25 -4
- bec_widgets/utils/bec_connector.py +66 -8
- bec_widgets/utils/colors.py +38 -0
- bec_widgets/utils/generate_designer_plugin.py +15 -14
- bec_widgets/utils/yaml_dialog.py +27 -3
- bec_widgets/widgets/console/console.py +496 -0
- bec_widgets/widgets/dock/dock.py +2 -2
- bec_widgets/widgets/dock/dock_area.py +2 -2
- bec_widgets/widgets/figure/figure.py +149 -195
- bec_widgets/widgets/figure/plots/image/image.py +62 -49
- bec_widgets/widgets/figure/plots/image/image_item.py +4 -3
- bec_widgets/widgets/figure/plots/motor_map/motor_map.py +98 -29
- bec_widgets/widgets/figure/plots/plot_base.py +1 -1
- bec_widgets/widgets/figure/plots/waveform/waveform.py +7 -8
- bec_widgets/widgets/figure/plots/waveform/waveform_curve.py +2 -2
- bec_widgets/widgets/ring_progress_bar/ring.py +3 -3
- bec_widgets/widgets/ring_progress_bar/ring_progress_bar.py +3 -3
- {bec_widgets-0.76.0.dist-info → bec_widgets-0.77.0.dist-info}/METADATA +2 -1
- {bec_widgets-0.76.0.dist-info → bec_widgets-0.77.0.dist-info}/RECORD +40 -38
- pyproject.toml +2 -1
- tests/end-2-end/test_bec_dock_rpc_e2e.py +16 -16
- tests/end-2-end/test_bec_figure_rpc_e2e.py +7 -7
- tests/end-2-end/test_rpc_register_e2e.py +8 -8
- tests/unit_tests/client_mocks.py +1 -0
- tests/unit_tests/test_bec_figure.py +49 -26
- tests/unit_tests/test_bec_motor_map.py +179 -41
- tests/unit_tests/test_color_validation.py +15 -0
- tests/unit_tests/test_device_input_base.py +1 -1
- tests/unit_tests/test_device_input_widgets.py +2 -0
- tests/unit_tests/test_generate_plugin.py +155 -0
- tests/unit_tests/test_motor_control.py +5 -4
- tests/unit_tests/test_plot_base.py +3 -3
- tests/unit_tests/test_waveform1d.py +18 -17
- tests/unit_tests/test_yaml_dialog.py +7 -7
- {bec_widgets-0.76.0.dist-info → bec_widgets-0.77.0.dist-info}/WHEEL +0 -0
- {bec_widgets-0.76.0.dist-info → bec_widgets-0.77.0.dist-info}/entry_points.txt +0 -0
- {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 = ["
|
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
|
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
|
139
|
+
def _rpc_id(self) -> str:
|
136
140
|
"""Get the RPC ID of the widget."""
|
137
141
|
return self.gui_id
|
138
142
|
|
139
|
-
@
|
140
|
-
def
|
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
|
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
|
-
@
|
155
|
-
def
|
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
|
"""
|
bec_widgets/utils/colors.py
CHANGED
@@ -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(
|
98
|
-
|
99
|
-
|
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
|
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
|
|
bec_widgets/utils/yaml_dialog.py
CHANGED
@@ -6,7 +6,7 @@ import yaml
|
|
6
6
|
from qtpy.QtWidgets import QFileDialog
|
7
7
|
|
8
8
|
|
9
|
-
def
|
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.
|
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
|
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:
|