bec-widgets 0.79.2__py3-none-any.whl → 0.80.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.
.gitlab-ci.yml CHANGED
@@ -154,7 +154,6 @@ test-matrix:
154
154
  - "3.12"
155
155
  QT_PCKG:
156
156
  - "pyside6"
157
- - "pyqt5"
158
157
  - "pyqt6"
159
158
 
160
159
  stage: AdditionalTests
.pylintrc CHANGED
@@ -3,7 +3,7 @@
3
3
  # A comma-separated list of package or module names from where C extensions may
4
4
  # be loaded. Extensions are loading into the active Python interpreter and may
5
5
  # run arbitrary code.
6
- extension-pkg-allow-list=PyQt5, pyqtgraph
6
+ extension-pkg-allow-list=PyQt6, PySide6, pyqtgraph
7
7
 
8
8
  # A comma-separated list of package or module names from where C extensions may
9
9
  # be loaded. Extensions are loading into the active Python interpreter and may
CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v0.80.0 (2024-07-06)
4
+
5
+ ### Feature
6
+
7
+ * feat(qt5): dropped support for qt5; pyside2 and pyqt5 ([`fadbf77`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fadbf77866903beff6580802bc203d53367fc7e7))
8
+
9
+ * feat(plugins): moved plugin dict to dataclass and container ([`03819a3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/03819a3d902b4a51f3e882d52aedd971b2a8e127))
10
+
11
+ * feat(plugins): added support for pyqt6 ui files ([`d6d0777`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d6d07771135335cb78dc648508ce573b8970261a))
12
+
13
+ * feat(plugins): added bec widgets base class ([`1aa83e0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1aa83e0ef1ffe45b01677b0b4590535cb0ca1cff))
14
+
15
+ ## v0.79.3 (2024-07-05)
16
+
17
+ ### Fix
18
+
19
+ * fix: changed inheritance to adress qt designer bug in rendering ([`e403870`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e403870874bd5e45840a034d6f1b3dd576d9c846))
20
+
21
+ * fix: add designer plugin classes ([`1586ce2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1586ce2d6cba2bb086b2ef596e724bb9e40ab4f2))
22
+
23
+ ### Refactor
24
+
25
+ * refactor: simplify logic in bec_status_box ([`576353c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/576353cfe8c6fd64db561f0b6e2bc951300643d3))
26
+
3
27
  ## v0.79.2 (2024-07-04)
4
28
 
5
29
  ### Fix
@@ -112,40 +136,10 @@
112
136
 
113
137
  ## v0.74.1 (2024-06-26)
114
138
 
115
- ### Build
116
-
117
- * build: added missing pytest-bec-e2e dependency; closes #219 ([`56fdae4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/56fdae42757bdb9fa301c1e425a77e98b6eaf92b))
118
-
119
- * build: fixed dependency ranges; closes #135 ([`e6a06c9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e6a06c9f43e0ad6bbfcfa550a2f580d2a27aff66))
120
-
121
- ### Chore
122
-
123
- * chore: sorted dependencies alphabetically ([`21c807f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/21c807f35831fdd1ef2e488ab90edae4719f0cb7))
124
-
125
- ### Documentation
126
-
127
- * docs: fixed doc string ([`f979a63`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f979a63d3d1a008f80e500510909750878ff4303))
128
-
129
139
  ### Fix
130
140
 
131
141
  * fix(rings): rings properties updated right after setting ([`c8b7367`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c8b7367815b095f8e4aa8b819481efb701f2e542))
132
142
 
133
- * fix(motor_map): motor map can be removed from BECFigure with .remove() ([`6b25abf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6b25abff70280271e2eeb70450553c05d4b7c99c))
134
-
135
143
  ### Test
136
144
 
137
145
  * test(bec_figure): tests for removing widgets with rpc e2e ([`a268caa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a268caaa30711fcc7ece542d24578d74cbf65c77))
138
-
139
- ## v0.74.0 (2024-06-25)
140
-
141
- ### Documentation
142
-
143
- * docs(becfigure): docs added ([`a51b15d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a51b15da3f5e83e0c897a0342bdb05b9c677a179))
144
-
145
- ### Feature
146
-
147
- * feat(waveform1d): dap LMFit model can be added to plot ([`1866ba6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1866ba66c8e3526661beb13fff3e13af6a0ae562))
148
-
149
- ### Test
150
-
151
- * test(waveform1d): dap e2e test added ([`7271b42`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7271b422f98ef9264970d708811c414b69a644db))
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bec_widgets
3
- Version: 0.79.2
3
+ Version: 0.80.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
@@ -28,9 +28,6 @@ Requires-Dist: pytest-random-order~=1.1; extra == 'dev'
28
28
  Requires-Dist: pytest-timeout~=2.2; extra == 'dev'
29
29
  Requires-Dist: pytest-xvfb~=3.0; extra == 'dev'
30
30
  Requires-Dist: pytest~=8.0; extra == 'dev'
31
- Provides-Extra: pyqt5
32
- Requires-Dist: pyqt5>=5.9; extra == 'pyqt5'
33
- Requires-Dist: pyqtwebengine>=5.9; extra == 'pyqt5'
34
31
  Provides-Extra: pyqt6
35
32
  Requires-Dist: pyqt6-webengine>=6.7; extra == 'pyqt6'
36
33
  Requires-Dist: pyqt6>=6.7; extra == 'pyqt6'
README.md CHANGED
@@ -17,7 +17,7 @@ cd bec_widgets
17
17
  pip install -e .[dev,pyqt6]
18
18
  ```
19
19
 
20
- BEC Widgets currently supports both PyQt5 and PyQt6, however, no default distribution is specified. As a result, users must install one of the supported
20
+ BEC Widgets currently supports both Pyside6 and PyQt6, however, no default distribution is specified. As a result, users must install one of the supported
21
21
  Python Qt distributions manually.
22
22
 
23
23
  To select a specific Python Qt distribution, install the package with an additional tag:
@@ -28,7 +28,7 @@ pip install bec_widgets[pyqt6]
28
28
  or
29
29
 
30
30
  ```bash
31
- pip install bec_widgets[pyqt5]
31
+ pip install bec_widgets[pyside6]
32
32
  ```
33
33
  ## Documentation
34
34
 
bec_widgets/cli/client.py CHANGED
@@ -13,12 +13,12 @@ class Widgets(str, enum.Enum):
13
13
  Enum for the available widgets.
14
14
  """
15
15
 
16
- BECQueue = "BECQueue"
17
- BECStatusBox = "BECStatusBox"
18
16
  BECDock = "BECDock"
19
17
  BECDockArea = "BECDockArea"
20
18
  BECFigure = "BECFigure"
21
19
  BECMotorMapWidget = "BECMotorMapWidget"
20
+ BECQueue = "BECQueue"
21
+ BECStatusBox = "BECStatusBox"
22
22
  RingProgressBar = "RingProgressBar"
23
23
  ScanControl = "ScanControl"
24
24
  TextBox = "TextBox"
@@ -5,13 +5,12 @@ import argparse
5
5
  import inspect
6
6
  import os
7
7
  import sys
8
- from typing import Literal
9
8
 
10
9
  import black
11
10
  import isort
12
11
 
13
12
  from bec_widgets.utils.generate_designer_plugin import DesignerPluginGenerator
14
- from bec_widgets.utils.plugin_utils import get_rpc_classes
13
+ from bec_widgets.utils.plugin_utils import BECClassContainer, get_rpc_classes
15
14
 
16
15
  if sys.version_info >= (3, 11):
17
16
  from typing import get_overloads
@@ -40,17 +39,20 @@ from bec_widgets.cli.client_utils import RPCBase, rpc_call, BECGuiClientMixin
40
39
 
41
40
  self.content = ""
42
41
 
43
- def generate_client(
44
- self, published_classes: dict[Literal["connector_classes", "top_level_classes"], list[type]]
45
- ):
42
+ def generate_client(self, class_container: BECClassContainer):
46
43
  """
47
44
  Generate the client for the published classes.
48
45
 
49
46
  Args:
50
- published_classes(dict): A dictionary with keys "connector_classes" and "top_level_classes" and values as lists of classes.
47
+ class_container: The class container with the classes to generate the client for.
51
48
  """
52
- self.write_client_enum(published_classes["top_level_classes"])
53
- for cls in published_classes["connector_classes"]:
49
+ rpc_top_level_classes = class_container.rpc_top_level_classes
50
+ rpc_top_level_classes.sort(key=lambda x: x.__name__)
51
+ connector_classes = class_container.connector_classes
52
+ connector_classes.sort(key=lambda x: x.__name__)
53
+
54
+ self.write_client_enum(rpc_top_level_classes)
55
+ for cls in connector_classes:
54
56
  self.content += "\n\n"
55
57
  self.generate_content_for_class(cls)
56
58
 
@@ -156,13 +158,12 @@ def main():
156
158
  client_path = os.path.join(current_path, "client.py")
157
159
 
158
160
  rpc_classes = get_rpc_classes("bec_widgets")
159
- rpc_classes["connector_classes"].sort(key=lambda x: x.__name__)
160
161
 
161
162
  generator = ClientGenerator()
162
163
  generator.generate_client(rpc_classes)
163
164
  generator.write(client_path)
164
165
 
165
- for cls in rpc_classes["top_level_classes"]:
166
+ for cls in rpc_classes.plugins:
166
167
  plugin = DesignerPluginGenerator(cls)
167
168
  if not hasattr(plugin, "info"):
168
169
  continue
@@ -29,7 +29,7 @@ class RPCWidgetHandler:
29
29
  from bec_widgets.utils.plugin_utils import get_rpc_classes
30
30
 
31
31
  clss = get_rpc_classes("bec_widgets")
32
- self._widget_classes = {cls.__name__: cls for cls in clss["top_level_classes"]}
32
+ self._widget_classes = {cls.__name__: cls for cls in clss.top_level_classes}
33
33
 
34
34
  def create_widget(self, widget_type, **kwargs) -> BECConnector:
35
35
  """
@@ -13,6 +13,7 @@ from qtpy.QtCore import QObject, QRunnable, QThreadPool, Signal
13
13
  from qtpy.QtCore import Slot as pyqtSlot
14
14
 
15
15
  from bec_widgets.cli.rpc_register import RPCRegister
16
+ from bec_widgets.utils.bec_widget import BECWidget
16
17
  from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
17
18
 
18
19
  BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
@@ -63,7 +64,7 @@ class Worker(QRunnable):
63
64
  self.signals.completed.emit()
64
65
 
65
66
 
66
- class BECConnector:
67
+ class BECConnector(BECWidget):
67
68
  """Connection mixin class for all BEC widgets, to handle BEC client and device manager"""
68
69
 
69
70
  USER_ACCESS = ["_config_dict", "_get_all_rpc"]
@@ -8,7 +8,7 @@ import redis
8
8
  from bec_lib.client import BECClient
9
9
  from bec_lib.redis_connector import MessageObject, RedisConnector
10
10
  from bec_lib.service_config import ServiceConfig
11
- from qtpy.QtCore import PYQT5, PYQT6, PYSIDE2, PYSIDE6, QCoreApplication, QObject
11
+ from qtpy.QtCore import PYQT6, PYSIDE6, QCoreApplication, QObject
12
12
  from qtpy.QtCore import Signal as pyqtSignal
13
13
 
14
14
  if TYPE_CHECKING:
@@ -127,9 +127,9 @@ class BECDispatcher:
127
127
  return
128
128
 
129
129
  # shutdown QCoreApp if it exists
130
- if PYQT5 or PYQT6:
130
+ if PYQT6:
131
131
  cls.qapp.exit()
132
- elif PYSIDE2 or PYSIDE6:
132
+ elif PYSIDE6:
133
133
  cls.qapp.shutdown()
134
134
  cls.qapp = None
135
135
 
@@ -0,0 +1,2 @@
1
+ class BECWidget:
2
+ """Base class for all BEC widgets."""
@@ -1,12 +1,13 @@
1
1
  import importlib
2
2
  import inspect
3
3
  import os
4
- from typing import Literal
4
+ from dataclasses import dataclass
5
5
 
6
6
  from bec_lib.plugin_helper import _get_available_plugins
7
7
  from qtpy.QtWidgets import QGraphicsWidget, QWidget
8
8
 
9
9
  from bec_widgets.utils import BECConnector
10
+ from bec_widgets.utils.bec_widget import BECWidget
10
11
 
11
12
 
12
13
  def get_plugin_widgets() -> dict[str, BECConnector]:
@@ -44,9 +45,74 @@ def _filter_plugins(obj):
44
45
  return inspect.isclass(obj) and issubclass(obj, BECConnector)
45
46
 
46
47
 
47
- def get_rpc_classes(
48
- repo_name: str,
49
- ) -> dict[Literal["connector_classes", "top_level_classes"], list[type]]:
48
+ @dataclass
49
+ class BECClassInfo:
50
+ name: str
51
+ module: str
52
+ file: str
53
+ obj: type
54
+ is_connector: bool = False
55
+ is_widget: bool = False
56
+ is_top_level: bool = False
57
+
58
+
59
+ class BECClassContainer:
60
+ def __init__(self):
61
+ self._collection = []
62
+
63
+ def add_class(self, class_info: BECClassInfo):
64
+ """
65
+ Add a class to the collection.
66
+
67
+ Args:
68
+ class_info(BECClassInfo): The class information
69
+ """
70
+ self.collection.append(class_info)
71
+
72
+ @property
73
+ def collection(self):
74
+ """
75
+ Get the collection of classes.
76
+ """
77
+ return self._collection
78
+
79
+ @property
80
+ def connector_classes(self):
81
+ """
82
+ Get all connector classes.
83
+ """
84
+ return [info.obj for info in self.collection if info.is_connector]
85
+
86
+ @property
87
+ def top_level_classes(self):
88
+ """
89
+ Get all top-level classes.
90
+ """
91
+ return [info.obj for info in self.collection if info.is_top_level]
92
+
93
+ @property
94
+ def plugins(self):
95
+ """
96
+ Get all plugins. These are all classes that are on the top level and are widgets.
97
+ """
98
+ return [info.obj for info in self.collection if info.is_widget and info.is_top_level]
99
+
100
+ @property
101
+ def widgets(self):
102
+ """
103
+ Get all widgets. These are all classes inheriting from BECWidget.
104
+ """
105
+ return [info.obj for info in self.collection if info.is_widget]
106
+
107
+ @property
108
+ def rpc_top_level_classes(self):
109
+ """
110
+ Get all top-level classes that are RPC-enabled. These are all classes that users can choose from.
111
+ """
112
+ return [info.obj for info in self.collection if info.is_top_level and info.is_connector]
113
+
114
+
115
+ def get_rpc_classes(repo_name: str) -> BECClassContainer:
50
116
  """
51
117
  Get all RPC-enabled classes in the specified repository.
52
118
 
@@ -56,8 +122,7 @@ def get_rpc_classes(
56
122
  Returns:
57
123
  dict: A dictionary with keys "connector_classes" and "top_level_classes" and values as lists of classes.
58
124
  """
59
- connector_classes = []
60
- top_level_classes = []
125
+ collection = BECClassContainer()
61
126
  anchor_module = importlib.import_module(f"{repo_name}.widgets")
62
127
  directory = os.path.dirname(anchor_module.__file__)
63
128
  for root, _, files in sorted(os.walk(directory)):
@@ -78,11 +143,16 @@ def get_rpc_classes(
78
143
  obj = getattr(module, name)
79
144
  if not hasattr(obj, "__module__") or obj.__module__ != module.__name__:
80
145
  continue
81
- if isinstance(obj, type) and issubclass(obj, BECConnector):
82
- connector_classes.append(obj)
146
+ if isinstance(obj, type):
147
+ class_info = BECClassInfo(name=name, module=module_name, file=path, obj=obj)
148
+ if issubclass(obj, BECConnector):
149
+ class_info.is_connector = True
150
+ if issubclass(obj, BECWidget):
151
+ class_info.is_widget = True
83
152
  if len(subs) == 1 and (
84
153
  issubclass(obj, QWidget) or issubclass(obj, QGraphicsWidget)
85
154
  ):
86
- top_level_classes.append(obj)
155
+ class_info.is_top_level = True
156
+ collection.add_class(class_info)
87
157
 
88
- return {"connector_classes": connector_classes, "top_level_classes": top_level_classes}
158
+ return collection
@@ -1,20 +1,18 @@
1
+ import os
2
+
1
3
  from qtpy import PYQT6, PYSIDE6, QT_VERSION
2
4
  from qtpy.QtCore import QFile, QIODevice
3
5
 
6
+ from bec_widgets.utils.generate_designer_plugin import DesignerPluginInfo
7
+ from bec_widgets.utils.plugin_utils import get_rpc_classes
8
+
4
9
  if PYSIDE6:
5
10
  from PySide6.QtUiTools import QUiLoader
6
11
 
7
- from bec_widgets.utils.plugin_utils import get_rpc_classes
8
- from bec_widgets.widgets.buttons.color_button.color_button import ColorButton
9
-
10
12
  class CustomUiLoader(QUiLoader):
11
- def __init__(self, baseinstance):
13
+ def __init__(self, baseinstance, custom_widgets: dict = None):
12
14
  super().__init__(baseinstance)
13
- widgets = get_rpc_classes("bec_widgets").get("top_level_classes", [])
14
-
15
- widgets.append(ColorButton)
16
-
17
- self.custom_widgets = {widget.__name__: widget for widget in widgets}
15
+ self.custom_widgets = custom_widgets or {}
18
16
 
19
17
  self.baseinstance = baseinstance
20
18
 
@@ -27,25 +25,21 @@ if PYSIDE6:
27
25
 
28
26
 
29
27
  class UILoader:
30
- """Universal UI loader for PyQt5, PyQt6, PySide2, and PySide6."""
28
+ """Universal UI loader for PyQt6 and PySide6."""
31
29
 
32
30
  def __init__(self, parent=None):
33
31
  self.parent = parent
34
- if QT_VERSION.startswith("5"):
35
- # PyQt5 or PySide2
36
- from qtpy import uic
37
-
38
- self.loader = uic.loadUi
39
- elif QT_VERSION.startswith("6"):
40
- # PyQt6 or PySide6
41
- if PYSIDE6:
42
- self.loader = self.load_ui_pyside6
43
- elif PYQT6:
44
- from PyQt6.uic import loadUi
45
-
46
- self.loader = loadUi
47
- else:
48
- raise ImportError("No compatible Qt bindings found.")
32
+
33
+ widgets = get_rpc_classes("bec_widgets").top_level_classes
34
+
35
+ self.custom_widgets = {widget.__name__: widget for widget in widgets}
36
+
37
+ if PYSIDE6:
38
+ self.loader = self.load_ui_pyside6
39
+ elif PYQT6:
40
+ self.loader = self.load_ui_pyqt6
41
+ else:
42
+ raise ImportError("No compatible Qt bindings found.")
49
43
 
50
44
  def load_ui_pyside6(self, ui_file, parent=None):
51
45
  """
@@ -58,7 +52,7 @@ class UILoader:
58
52
  QWidget: The loaded widget.
59
53
  """
60
54
 
61
- loader = CustomUiLoader(parent)
55
+ loader = CustomUiLoader(parent, self.custom_widgets)
62
56
  file = QFile(ui_file)
63
57
  if not file.open(QIODevice.ReadOnly):
64
58
  raise IOError(f"Cannot open file: {ui_file}")
@@ -66,6 +60,71 @@ class UILoader:
66
60
  file.close()
67
61
  return widget
68
62
 
63
+ def load_ui_pyqt6(self, ui_file, parent=None):
64
+ """
65
+ Specific loader for PyQt6 using loadUi.
66
+ Args:
67
+ ui_file(str): Path to the .ui file.
68
+ parent(QWidget): Parent widget.
69
+
70
+ Returns:
71
+ QWidget: The loaded widget.
72
+ """
73
+ from PyQt6.uic.Loader.loader import DynamicUILoader
74
+
75
+ class CustomDynamicUILoader(DynamicUILoader):
76
+ def __init__(self, package, custom_widgets: dict = None):
77
+ super().__init__(package)
78
+ self.custom_widgets = custom_widgets or {}
79
+
80
+ def _handle_custom_widgets(self, el):
81
+ """Handle the <customwidgets> element."""
82
+
83
+ def header2module(header):
84
+ """header2module(header) -> string
85
+
86
+ Convert paths to C++ header files to according Python modules
87
+ >>> header2module("foo/bar/baz.h")
88
+ 'foo.bar.baz'
89
+ """
90
+
91
+ if header.endswith(".h"):
92
+ header = header[:-2]
93
+
94
+ mpath = []
95
+ for part in header.split("/"):
96
+ # Ignore any empty parts or those that refer to the current
97
+ # directory.
98
+ if part not in ("", "."):
99
+ if part == "..":
100
+ # We should allow this for Python3.
101
+ raise SyntaxError(
102
+ "custom widget header file name may not contain '..'."
103
+ )
104
+
105
+ mpath.append(part)
106
+
107
+ return ".".join(mpath)
108
+
109
+ for custom_widget in el:
110
+ classname = custom_widget.findtext("class")
111
+ header = custom_widget.findtext("header")
112
+ if header:
113
+ header = self._translate_bec_widgets_header(header)
114
+ self.factory.addCustomWidget(
115
+ classname,
116
+ custom_widget.findtext("extends") or "QWidget",
117
+ header2module(header),
118
+ )
119
+
120
+ def _translate_bec_widgets_header(self, header):
121
+ for name, value in self.custom_widgets.items():
122
+ if header == DesignerPluginInfo.pascal_to_snake(name):
123
+ return value.__module__
124
+ return header
125
+
126
+ return CustomDynamicUILoader("", self.custom_widgets).loadUi(ui_file, parent)
127
+
69
128
  def load_ui(self, ui_file, parent=None):
70
129
  """
71
130
  Universal UI loader method.