bec-widgets 0.76.1__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 (38) hide show
  1. CHANGELOG.md +36 -38
  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/yaml_dialog.py +27 -3
  8. bec_widgets/widgets/console/console.py +496 -0
  9. bec_widgets/widgets/dock/dock.py +2 -2
  10. bec_widgets/widgets/dock/dock_area.py +2 -2
  11. bec_widgets/widgets/figure/figure.py +149 -195
  12. bec_widgets/widgets/figure/plots/image/image.py +62 -49
  13. bec_widgets/widgets/figure/plots/image/image_item.py +4 -3
  14. bec_widgets/widgets/figure/plots/motor_map/motor_map.py +98 -29
  15. bec_widgets/widgets/figure/plots/plot_base.py +1 -1
  16. bec_widgets/widgets/figure/plots/waveform/waveform.py +7 -8
  17. bec_widgets/widgets/figure/plots/waveform/waveform_curve.py +2 -2
  18. bec_widgets/widgets/ring_progress_bar/ring.py +3 -3
  19. bec_widgets/widgets/ring_progress_bar/ring_progress_bar.py +3 -3
  20. {bec_widgets-0.76.1.dist-info → bec_widgets-0.77.0.dist-info}/METADATA +2 -1
  21. {bec_widgets-0.76.1.dist-info → bec_widgets-0.77.0.dist-info}/RECORD +38 -37
  22. pyproject.toml +2 -1
  23. tests/end-2-end/test_bec_dock_rpc_e2e.py +16 -16
  24. tests/end-2-end/test_bec_figure_rpc_e2e.py +7 -7
  25. tests/end-2-end/test_rpc_register_e2e.py +8 -8
  26. tests/unit_tests/client_mocks.py +1 -0
  27. tests/unit_tests/test_bec_figure.py +49 -26
  28. tests/unit_tests/test_bec_motor_map.py +179 -41
  29. tests/unit_tests/test_color_validation.py +15 -0
  30. tests/unit_tests/test_device_input_base.py +1 -1
  31. tests/unit_tests/test_device_input_widgets.py +2 -0
  32. tests/unit_tests/test_motor_control.py +5 -4
  33. tests/unit_tests/test_plot_base.py +3 -3
  34. tests/unit_tests/test_waveform1d.py +18 -17
  35. tests/unit_tests/test_yaml_dialog.py +7 -7
  36. {bec_widgets-0.76.1.dist-info → bec_widgets-0.77.0.dist-info}/WHEEL +0 -0
  37. {bec_widgets-0.76.1.dist-info → bec_widgets-0.77.0.dist-info}/entry_points.txt +0 -0
  38. {bec_widgets-0.76.1.dist-info → bec_widgets-0.77.0.dist-info}/licenses/LICENSE +0 -0
@@ -8,7 +8,7 @@ from typing import Literal, Optional
8
8
  import numpy as np
9
9
  import pyqtgraph as pg
10
10
  import qdarktheme
11
- from pydantic import Field
11
+ from pydantic import Field, ValidationError, field_validator
12
12
  from qtpy.QtCore import Signal as pyqtSignal
13
13
  from qtpy.QtWidgets import QWidget
14
14
  from typeguard import typechecked
@@ -30,16 +30,36 @@ class FigureConfig(ConnectionConfig):
30
30
  {}, description="The list of widgets to be added to the figure widget."
31
31
  )
32
32
 
33
+ @field_validator("widgets", mode="before")
34
+ @classmethod
35
+ def validate_widgets(cls, v):
36
+ """Validate the widgets configuration."""
37
+ widget_class_map = {
38
+ "BECWaveform": Waveform1DConfig,
39
+ "BECImageShow": ImageConfig,
40
+ "BECMotorMap": MotorMapConfig,
41
+ }
42
+ validated_widgets = {}
43
+ for key, widget_config in v.items():
44
+ if "widget_class" not in widget_config:
45
+ raise ValueError(f"Widget config for {key} does not contain 'widget_class'.")
46
+ widget_class = widget_config["widget_class"]
47
+ if widget_class not in widget_class_map:
48
+ raise ValueError(f"Unknown widget_class '{widget_class}' for widget '{key}'.")
49
+ config_class = widget_class_map[widget_class]
50
+ validated_widgets[key] = config_class(**widget_config)
51
+ return validated_widgets
52
+
33
53
 
34
54
  class WidgetHandler:
35
55
  """Factory for creating and configuring BEC widgets for BECFigure."""
36
56
 
37
57
  def __init__(self):
38
58
  self.widget_factory = {
39
- "PlotBase": (BECPlotBase, SubplotConfig),
40
- "Waveform1D": (BECWaveform, Waveform1DConfig),
41
- "ImShow": (BECImageShow, ImageConfig),
42
- "MotorMap": (BECMotorMap, MotorMapConfig),
59
+ "BECPlotBase": (BECPlotBase, SubplotConfig),
60
+ "BECWaveform": (BECWaveform, Waveform1DConfig),
61
+ "BECImageShow": (BECImageShow, ImageConfig),
62
+ "BECMotorMap": (BECMotorMap, MotorMapConfig),
43
63
  }
44
64
 
45
65
  def create_widget(
@@ -90,13 +110,11 @@ class WidgetHandler:
90
110
 
91
111
  class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
92
112
  USER_ACCESS = [
93
- "rpc_id",
94
- "config_dict",
113
+ "_rpc_id",
114
+ "_config_dict",
115
+ "_get_all_rpc",
95
116
  "axes",
96
117
  "widgets",
97
- "add_plot",
98
- "add_image",
99
- "add_motor_map",
100
118
  "plot",
101
119
  "image",
102
120
  "motor_map",
@@ -104,9 +122,15 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
104
122
  "change_layout",
105
123
  "change_theme",
106
124
  "clear_all",
107
- "get_all_rpc",
108
125
  "widget_list",
109
126
  ]
127
+ subplot_map = {
128
+ "PlotBase": BECPlotBase,
129
+ "BECWaveform": BECWaveform,
130
+ "BECImageShow": BECImageShow,
131
+ "BECMotorMap": BECMotorMap,
132
+ }
133
+ widget_method_map = {"BECWaveform": "plot", "BECImageShow": "image", "BECMotorMap": "motor_map"}
110
134
 
111
135
  clean_signal = pyqtSignal()
112
136
 
@@ -122,8 +146,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
122
146
  else:
123
147
  if isinstance(config, dict):
124
148
  config = FigureConfig(**config)
125
- self.config = config
126
- super().__init__(client=client, config=config, gui_id=gui_id)
149
+ super().__init__(client=client, gui_id=gui_id)
127
150
  pg.GraphicsLayoutWidget.__init__(self, parent)
128
151
 
129
152
  self.widget_handler = WidgetHandler()
@@ -133,6 +156,8 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
133
156
 
134
157
  # Container to keep track of the grid
135
158
  self.grid = []
159
+ # Create config and apply it
160
+ self.apply_config(config)
136
161
 
137
162
  def __getitem__(self, key: tuple | str):
138
163
  if isinstance(key, tuple) and len(key) == 2:
@@ -147,6 +172,24 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
147
172
  "Key must be a string (widget id) or a tuple of two integers (grid coordinates)"
148
173
  )
149
174
 
175
+ def apply_config(self, config: dict | FigureConfig): # ,generate_new_id: bool = False):
176
+ if isinstance(config, dict):
177
+ try:
178
+ config = FigureConfig(**config)
179
+ except ValidationError as e:
180
+ print(f"Error in applying config: {e}")
181
+ return
182
+ self.config = config
183
+ self.change_theme(self.config.theme)
184
+
185
+ # widget_config has to be reset for not have each widget config twice when added to the figure
186
+ widget_configs = [config for config in self.config.widgets.values()]
187
+ self.config.widgets = {}
188
+ for widget_config in widget_configs:
189
+ getattr(self, self.widget_method_map[widget_config.widget_class])(
190
+ config=widget_config.model_dump(), row=widget_config.row, col=widget_config.col
191
+ )
192
+
150
193
  @property
151
194
  def widget_list(self) -> list[BECPlotBase]:
152
195
  """
@@ -200,7 +243,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
200
243
  label: str | None = None,
201
244
  validate: bool = True,
202
245
  dap: str | None = None,
203
- ):
246
+ ) -> BECWaveform:
204
247
  """
205
248
  Configure the waveform based on the provided parameters.
206
249
 
@@ -279,75 +322,6 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
279
322
 
280
323
  return waveform
281
324
 
282
- def add_plot(
283
- self,
284
- x: list | np.ndarray = None,
285
- y: list | np.ndarray = None,
286
- x_name: str = None,
287
- y_name: str = None,
288
- z_name: str = None,
289
- x_entry: str = None,
290
- y_entry: str = None,
291
- z_entry: str = None,
292
- color: Optional[str] = None,
293
- color_map_z: Optional[str] = "plasma",
294
- label: Optional[str] = None,
295
- validate: bool = True,
296
- row: int = None,
297
- col: int = None,
298
- config=None,
299
- dap: str | None = None,
300
- **axis_kwargs,
301
- ) -> BECWaveform:
302
- """
303
- Add a Waveform1D plot to the figure at the specified position.
304
-
305
- Args:
306
- x(list | np.ndarray): Custom x data to plot.
307
- y(list | np.ndarray): Custom y data to plot.
308
- x_name(str): The name of the device for the x-axis.
309
- y_name(str): The name of the device for the y-axis.
310
- z_name(str): The name of the device for the z-axis.
311
- x_entry(str): The name of the entry for the x-axis.
312
- y_entry(str): The name of the entry for the y-axis.
313
- z_entry(str): The name of the entry for the z-axis.
314
- color(str): The color of the curve.
315
- color_map_z(str): The color map to use for the z-axis.
316
- label(str): The label of the curve.
317
- validate(bool): If True, validate the device names and entries.
318
- row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
319
- col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
320
- config(dict): Additional configuration for the widget.
321
- **axis_kwargs(dict): Additional axis properties to set on the widget after creation.
322
- """
323
- widget_id = str(uuid.uuid4())
324
- waveform = self.add_widget(
325
- widget_type="Waveform1D",
326
- widget_id=widget_id,
327
- row=row,
328
- col=col,
329
- config=config,
330
- **axis_kwargs,
331
- )
332
-
333
- waveform = self._init_waveform(
334
- waveform=waveform,
335
- x=x,
336
- y=y,
337
- x_name=x_name,
338
- y_name=y_name,
339
- z_name=z_name,
340
- x_entry=x_entry,
341
- y_entry=y_entry,
342
- z_entry=z_entry,
343
- color=color,
344
- color_map_z=color_map_z,
345
- label=label,
346
- validate=validate,
347
- dap=dap,
348
- )
349
- return waveform
350
-
351
325
  @typechecked
352
326
  def plot(
353
327
  self,
@@ -363,7 +337,11 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
363
337
  color_map_z: str | None = "plasma",
364
338
  label: str | None = None,
365
339
  validate: bool = True,
340
+ new: bool = False,
341
+ row: int | None = None,
342
+ col: int | None = None,
366
343
  dap: str | None = None,
344
+ config: dict | None = None, # TODO make logic more transparent
367
345
  **axis_kwargs,
368
346
  ) -> BECWaveform:
369
347
  """
@@ -382,21 +360,23 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
382
360
  color_map_z(str): The color map to use for the z-axis.
383
361
  label(str): The label of the curve.
384
362
  validate(bool): If True, validate the device names and entries.
363
+ new(bool): If True, create a new plot instead of using the first plot.
364
+ row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
365
+ col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
385
366
  dap(str): The DAP model to use for the curve.
367
+ config(dict): Recreates the whole BECWaveform widget from provided configuration.
386
368
  **axis_kwargs: Additional axis properties to set on the widget after creation.
387
369
 
388
370
  Returns:
389
371
  BECWaveform: The waveform plot widget.
390
372
  """
391
- waveform = WidgetContainerUtils.find_first_widget_by_class(
392
- self._widgets, BECWaveform, can_fail=True
373
+ waveform = self.subplot_factory(
374
+ widget_type="BECWaveform", config=config, row=row, col=col, new=new, **axis_kwargs
393
375
  )
394
- if waveform is not None:
395
- if axis_kwargs:
396
- waveform.set(**axis_kwargs)
397
- else:
398
- waveform = self.add_plot(**axis_kwargs)
376
+ if config is not None:
377
+ return waveform
399
378
 
379
+ # Passing args to init_waveform
400
380
  waveform = self._init_waveform(
401
381
  waveform=waveform,
402
382
  x=x,
@@ -413,7 +393,6 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
413
393
  validate=validate,
414
394
  dap=dap,
415
395
  )
416
- # TODO remove repetition from .plot method
417
396
  return waveform
418
397
 
419
398
  def _init_image(
@@ -460,6 +439,10 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
460
439
  color_map: str = "magma",
461
440
  data: np.ndarray = None,
462
441
  vrange: tuple[float, float] = None,
442
+ new: bool = False,
443
+ row: int | None = None,
444
+ col: int | None = None,
445
+ config: dict | None = None,
463
446
  **axis_kwargs,
464
447
  ) -> BECImageShow:
465
448
  """
@@ -471,78 +454,22 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
471
454
  color_map(str): The color map to use for the image.
472
455
  data(np.ndarray): Custom data to display.
473
456
  vrange(tuple[float, float]): The range of values to display.
474
- **axis_kwargs: Additional axis properties to set on the widget after creation.
475
-
476
- Returns:
477
- BECImageShow: The image widget.
478
- """
479
- image = WidgetContainerUtils.find_first_widget_by_class(
480
- self._widgets, BECImageShow, can_fail=True
481
- )
482
- if image is not None:
483
- if axis_kwargs:
484
- image.set(**axis_kwargs)
485
- else:
486
- image = self.add_image(color_bar=color_bar, **axis_kwargs)
487
-
488
- image = self._init_image(
489
- image=image,
490
- monitor=monitor,
491
- color_bar=color_bar,
492
- color_map=color_map,
493
- data=data,
494
- vrange=vrange,
495
- )
496
- return image
497
-
498
- def add_image(
499
- self,
500
- monitor: str = None,
501
- color_bar: Literal["simple", "full"] = "full",
502
- color_map: str = "magma",
503
- data: np.ndarray = None,
504
- vrange: tuple[float, float] = None,
505
- row: int = None,
506
- col: int = None,
507
- config=None,
508
- **axis_kwargs,
509
- ) -> BECImageShow:
510
- """
511
- Add an image to the figure at the specified position.
512
-
513
- Args:
514
- monitor(str): The name of the monitor to display.
515
- color_bar(Literal["simple","full"]): The type of color bar to display.
516
- color_map(str): The color map to use for the image.
517
- data(np.ndarray): Custom data to display.
518
- vrange(tuple[float, float]): The range of values to display.
457
+ new(bool): If True, create a new plot instead of using the first plot.
519
458
  row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
520
459
  col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
521
- config(dict): Additional configuration for the widget.
460
+ config(dict): Recreates the whole BECImageShow widget from provided configuration.
522
461
  **axis_kwargs: Additional axis properties to set on the widget after creation.
523
462
 
524
463
  Returns:
525
464
  BECImageShow: The image widget.
526
465
  """
527
466
 
528
- widget_id = str(uuid.uuid4())
529
- if config is None:
530
- config = ImageConfig(
531
- widget_class="BECImageShow",
532
- gui_id=widget_id,
533
- parent_id=self.gui_id,
534
- color_map=color_map,
535
- color_bar=color_bar,
536
- vrange=vrange,
537
- )
538
- image = self.add_widget(
539
- widget_type="ImShow",
540
- widget_id=widget_id,
541
- row=row,
542
- col=col,
543
- config=config,
544
- **axis_kwargs,
467
+ image = self.subplot_factory(
468
+ widget_type="BECImageShow", config=config, row=row, col=col, new=new, **axis_kwargs
545
469
  )
470
+ if config is not None:
471
+ return image
472
+
546
473
  image = self._init_image(
547
474
  image=image,
548
475
  monitor=monitor,
@@ -553,76 +480,99 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
553
480
  )
554
481
  return image
555
482
 
556
- def motor_map(self, motor_x: str = None, motor_y: str = None, **axis_kwargs) -> BECMotorMap:
483
+ def motor_map(
484
+ self,
485
+ motor_x: str = None,
486
+ motor_y: str = None,
487
+ new: bool = False,
488
+ row: int | None = None,
489
+ col: int | None = None,
490
+ config: dict | None = None,
491
+ **axis_kwargs,
492
+ ) -> BECMotorMap:
557
493
  """
558
494
  Add a motor map to the figure. Always access the first motor map widget in the figure.
559
495
 
560
496
  Args:
561
497
  motor_x(str): The name of the motor for the X axis.
562
498
  motor_y(str): The name of the motor for the Y axis.
499
+ new(bool): If True, create a new plot instead of using the first plot.
500
+ row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
501
+ col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
502
+ config(dict): Recreates the whole BECImageShow widget from provided configuration.
563
503
  **axis_kwargs: Additional axis properties to set on the widget after creation.
564
504
 
565
505
  Returns:
566
506
  BECMotorMap: The motor map widget.
567
507
  """
568
- motor_map = WidgetContainerUtils.find_first_widget_by_class(
569
- self._widgets, BECMotorMap, can_fail=True
508
+ motor_map = self.subplot_factory(
509
+ widget_type="BECMotorMap", config=config, row=row, col=col, new=new, **axis_kwargs
570
510
  )
571
- if motor_map is not None:
572
- if axis_kwargs:
573
- motor_map.set(**axis_kwargs)
574
- else:
575
- motor_map = self.add_motor_map(**axis_kwargs)
511
+ if config is not None:
512
+ return motor_map
576
513
 
577
514
  if motor_x is not None and motor_y is not None:
578
515
  motor_map.change_motors(motor_x, motor_y)
579
516
 
580
517
  return motor_map
581
518
 
582
- def add_motor_map(
519
+ def subplot_factory(
583
520
  self,
584
- motor_x: str = None,
585
- motor_y: str = None,
521
+ widget_type: Literal[
522
+ "BECPlotBase", "BECWaveform", "BECImageShow", "BECMotorMap"
523
+ ] = "BECPlotBase",
586
524
  row: int = None,
587
525
  col: int = None,
588
526
  config=None,
527
+ new: bool = False,
589
528
  **axis_kwargs,
590
- ) -> BECMotorMap:
591
- """
592
-
593
- Args:
594
- motor_x(str): The name of the motor for the X axis.
595
- motor_y(str): The name of the motor for the Y axis.
596
- row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
597
- col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
598
- config(dict): Additional configuration for the widget.
599
- **axis_kwargs:
600
-
601
- Returns:
602
- BECMotorMap: The motor map widget.
603
- """
604
- widget_id = str(uuid.uuid4())
605
- if config is None:
606
- config = MotorMapConfig(
607
- widget_class="BECMotorMap", gui_id=widget_id, parent_id=self.gui_id
529
+ ) -> BECPlotBase:
530
+ # Case 1 - config provided, new plot, possible to define coordinates
531
+ if config is not None:
532
+ widget_cls = config["widget_class"]
533
+ if widget_cls != widget_type:
534
+ raise ValueError(
535
+ f"Widget type '{widget_type}' does not match the provided configuration ({widget_cls})."
536
+ )
537
+ widget = self.add_widget(
538
+ widget_type=widget_type, config=config, row=row, col=col, **axis_kwargs
608
539
  )
609
- motor_map = self.add_widget(
610
- widget_type="MotorMap",
611
- widget_id=widget_id,
612
- row=row,
613
- col=col,
614
- config=config,
615
- **axis_kwargs,
616
- )
540
+ return widget
617
541
 
618
- if motor_x is not None and motor_y is not None:
619
- motor_map.change_motors(motor_x, motor_y)
542
+ # Case 2 - find first plot or create first plot if no plot available, no config provided, no coordinates
543
+ if new is False and (row is None or col is None):
544
+ widget = WidgetContainerUtils.find_first_widget_by_class(
545
+ self._widgets, self.subplot_map[widget_type], can_fail=True
546
+ )
547
+ if widget is not None:
548
+ if axis_kwargs:
549
+ widget.set(**axis_kwargs)
550
+ else:
551
+ widget = self.add_widget(widget_type=widget_type, **axis_kwargs)
552
+ return widget
553
+
554
+ # Case 3 - modifying existing plot wit coordinates provided
555
+ if new is False and (row is not None and col is not None):
556
+ try:
557
+ widget = self.axes(row, col)
558
+ except ValueError:
559
+ widget = None
560
+ if widget is not None:
561
+ if axis_kwargs:
562
+ widget.set(**axis_kwargs)
563
+ else:
564
+ widget = self.add_widget(widget_type=widget_type, row=row, col=col, **axis_kwargs)
565
+ return widget
620
566
 
621
- return motor_map
567
+ # Case 4 - no previous plot or new plot, no config provided, possible to define coordinates
568
+ widget = self.add_widget(widget_type=widget_type, row=row, col=col, **axis_kwargs)
569
+ return widget
622
570
 
623
571
  def add_widget(
624
572
  self,
625
- widget_type: Literal["PlotBase", "Waveform1D", "ImShow"] = "PlotBase",
573
+ widget_type: Literal[
574
+ "BECPlotBase", "BECWaveform", "BECImageShow", "BECMotorMap"
575
+ ] = "BECPlotBase",
626
576
  widget_id: str = None,
627
577
  row: int = None,
628
578
  col: int = None,
@@ -653,6 +603,9 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
653
603
  config=config,
654
604
  **axis_kwargs,
655
605
  )
606
+ # has to be changed manually to ensure unique id, if config is copied from existing widget, the id could be
607
+ # used otherwise multiple times
608
+ widget.set_gui_id(widget_id)
656
609
 
657
610
  # Check if position is occupied
658
611
  if row is not None and col is not None:
@@ -756,6 +709,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
756
709
  self._reindex_grid()
757
710
  if widget_id in self.config.widgets:
758
711
  self.config.widgets.pop(widget_id)
712
+ widget.deleteLater()
759
713
  else:
760
714
  raise ValueError(f"Widget with ID '{widget_id}' does not exist.")
761
715