bec-widgets 1.14.1__py3-none-any.whl → 1.15.1__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 CHANGED
@@ -1,6 +1,25 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v1.15.1 (2025-01-13)
5
+
6
+ ### Bug Fixes
7
+
8
+ - **error_popups**: Safeproperty wrapper extended to catch more errors and not crash Designer
9
+ ([`3b04b98`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3b04b985b66a7237703a87f6a53610171eb9ffa5))
10
+
11
+
12
+ ## v1.15.0 (2025-01-10)
13
+
14
+ ### Features
15
+
16
+ - **widget_state_manager**: Example app added
17
+ ([`a00d368`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a00d368c25a19b04d6fbc8a07cff330d1a232e21))
18
+
19
+ - **widget_state_manager**: State manager for single widget
20
+ ([`01b4608`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/01b4608331f375aeeeb692328b693f2d2802dc9c))
21
+
22
+
4
23
  ## v1.14.1 (2025-01-10)
5
24
 
6
25
  ### Bug Fixes
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 1.14.1
3
+ Version: 1.15.1
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
@@ -2,25 +2,52 @@ import functools
2
2
  import sys
3
3
  import traceback
4
4
 
5
+ from bec_lib.logger import bec_logger
5
6
  from qtpy.QtCore import Property, QObject, Qt, Signal, Slot
6
7
  from qtpy.QtWidgets import QApplication, QMessageBox, QPushButton, QVBoxLayout, QWidget
7
8
 
8
9
 
9
- def SafeProperty(prop_type, *prop_args, popup_error: bool = False, **prop_kwargs):
10
+ def SafeProperty(prop_type, *prop_args, popup_error: bool = False, default=None, **prop_kwargs):
10
11
  """
11
- Decorator to create a Qt Property with a safe setter that won't crash Designer on errors.
12
- Behaves similarly to SafeSlot, but for properties.
12
+ Decorator to create a Qt Property with safe getter and setter so that
13
+ Qt Designer won't crash if an exception occurs in either method.
13
14
 
14
15
  Args:
15
- prop_type: The property type (e.g., str, bool, "QStringList", etc.)
16
- popup_error (bool): If True, show popup on error, otherwise just handle it silently.
17
- *prop_args, **prop_kwargs: Additional arguments and keyword arguments accepted by Property.
16
+ prop_type: The property type (e.g., str, bool, int, custom classes, etc.)
17
+ popup_error (bool): If True, show a popup for any error; otherwise, ignore or log silently.
18
+ default: Any default/fallback value to return if the getter raises an exception.
19
+ *prop_args, **prop_kwargs: Passed along to the underlying Qt Property constructor.
20
+
21
+ Usage:
22
+ @SafeProperty(int, default=-1)
23
+ def some_value(self) -> int:
24
+ # your getter logic
25
+ return ... # if an exception is raised, returns -1
26
+
27
+ @some_value.setter
28
+ def some_value(self, val: int):
29
+ # your setter logic
30
+ ...
18
31
  """
19
32
 
20
- def decorator(getter):
33
+ def decorator(py_getter):
34
+ @functools.wraps(py_getter)
35
+ def safe_getter(self_):
36
+ try:
37
+ return py_getter(self_)
38
+ except Exception:
39
+ if popup_error:
40
+ ErrorPopupUtility().custom_exception_hook(*sys.exc_info(), popup_error=True)
41
+ # Return the user-defined default (which might be anything, including None).
42
+ else:
43
+ error_msg = traceback.format_exc()
44
+ bec_logger.error(error_msg)
45
+ return default
46
+
21
47
  class PropertyWrapper:
22
48
  def __init__(self, getter_func):
23
- self.getter_func = getter_func
49
+ # We store only our safe_getter in the wrapper
50
+ self.getter_func = safe_getter
24
51
 
25
52
  def setter(self, setter_func):
26
53
  @functools.wraps(setter_func)
@@ -32,12 +59,20 @@ def SafeProperty(prop_type, *prop_args, popup_error: bool = False, **prop_kwargs
32
59
  ErrorPopupUtility().custom_exception_hook(
33
60
  *sys.exc_info(), popup_error=True
34
61
  )
62
+ # Swallow the exception; no crash in Designer
35
63
  else:
36
- return
64
+ error_msg = traceback.format_exc()
65
+ bec_logger.error(error_msg)
66
+ return
37
67
 
68
+ # Return the full read/write Property
38
69
  return Property(prop_type, self.getter_func, safe_setter, *prop_args, **prop_kwargs)
39
70
 
40
- return PropertyWrapper(getter)
71
+ def __call__(self):
72
+ # If the user never chains a .setter(...) call, we produce a read-only property
73
+ return Property(prop_type, self.getter_func, None, *prop_args, **prop_kwargs)
74
+
75
+ return PropertyWrapper(py_getter)
41
76
 
42
77
  return decorator
43
78
 
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ from qtpy.QtCore import QSettings
4
+ from qtpy.QtWidgets import (
5
+ QApplication,
6
+ QCheckBox,
7
+ QFileDialog,
8
+ QHBoxLayout,
9
+ QLineEdit,
10
+ QPushButton,
11
+ QSpinBox,
12
+ QVBoxLayout,
13
+ QWidget,
14
+ )
15
+
16
+
17
+ class WidgetStateManager:
18
+ """
19
+ A class to manage the state of a widget by saving and loading the state to and from a INI file.
20
+
21
+ Args:
22
+ widget(QWidget): The widget to manage the state for.
23
+ """
24
+
25
+ def __init__(self, widget):
26
+ self.widget = widget
27
+
28
+ def save_state(self, filename: str = None):
29
+ """
30
+ Save the state of the widget to a INI file.
31
+
32
+ Args:
33
+ filename(str): The filename to save the state to.
34
+ """
35
+ if not filename:
36
+ filename, _ = QFileDialog.getSaveFileName(
37
+ self.widget, "Save Settings", "", "INI Files (*.ini)"
38
+ )
39
+ if filename:
40
+ settings = QSettings(filename, QSettings.IniFormat)
41
+ self._save_widget_state_qsettings(self.widget, settings)
42
+
43
+ def load_state(self, filename: str = None):
44
+ """
45
+ Load the state of the widget from a INI file.
46
+
47
+ Args:
48
+ filename(str): The filename to load the state from.
49
+ """
50
+ if not filename:
51
+ filename, _ = QFileDialog.getOpenFileName(
52
+ self.widget, "Load Settings", "", "INI Files (*.ini)"
53
+ )
54
+ if filename:
55
+ settings = QSettings(filename, QSettings.IniFormat)
56
+ self._load_widget_state_qsettings(self.widget, settings)
57
+
58
+ def _save_widget_state_qsettings(self, widget: QWidget, settings: QSettings):
59
+ """
60
+ Save the state of the widget to QSettings.
61
+
62
+ Args:
63
+ widget(QWidget): The widget to save the state for.
64
+ settings(QSettings): The QSettings object to save the state to.
65
+ """
66
+ meta = widget.metaObject()
67
+ settings.beginGroup(widget.objectName())
68
+ for i in range(meta.propertyCount()):
69
+ prop = meta.property(i)
70
+ name = prop.name()
71
+ value = widget.property(name)
72
+ settings.setValue(name, value)
73
+ settings.endGroup()
74
+
75
+ # Recursively save child widgets
76
+ for child in widget.findChildren(QWidget):
77
+ if child.objectName():
78
+ self._save_widget_state_qsettings(child, settings)
79
+
80
+ def _load_widget_state_qsettings(self, widget: QWidget, settings: QSettings):
81
+ """
82
+ Load the state of the widget from QSettings.
83
+
84
+ Args:
85
+ widget(QWidget): The widget to load the state for.
86
+ settings(QSettings): The QSettings object to load the state from.
87
+ """
88
+ meta = widget.metaObject()
89
+ settings.beginGroup(widget.objectName())
90
+ for i in range(meta.propertyCount()):
91
+ prop = meta.property(i)
92
+ name = prop.name()
93
+ if settings.contains(name):
94
+ value = settings.value(name)
95
+ widget.setProperty(name, value)
96
+ settings.endGroup()
97
+
98
+ # Recursively load child widgets
99
+ for child in widget.findChildren(QWidget):
100
+ if child.objectName():
101
+ self._load_widget_state_qsettings(child, settings)
102
+
103
+
104
+ class ExampleApp(QWidget): # pragma: no cover
105
+ def __init__(self):
106
+ super().__init__()
107
+ self.setObjectName("MainWindow")
108
+ self.setWindowTitle("State Manager Example")
109
+
110
+ layout = QVBoxLayout(self)
111
+
112
+ # A line edit to store some user text
113
+ self.line_edit = QLineEdit(self)
114
+ self.line_edit.setObjectName("MyLineEdit")
115
+ self.line_edit.setPlaceholderText("Enter some text here...")
116
+ layout.addWidget(self.line_edit)
117
+
118
+ # A spin box to hold a numeric value
119
+ self.spin_box = QSpinBox(self)
120
+ self.spin_box.setObjectName("MySpinBox")
121
+ self.spin_box.setRange(0, 100)
122
+ layout.addWidget(self.spin_box)
123
+
124
+ # A checkbox to hold a boolean value
125
+ self.check_box = QCheckBox("Enable feature?", self)
126
+ self.check_box.setObjectName("MyCheckBox")
127
+ layout.addWidget(self.check_box)
128
+
129
+ # Buttons to save and load state
130
+ button_layout = QHBoxLayout()
131
+ self.save_button = QPushButton("Save State", self)
132
+ self.load_button = QPushButton("Load State", self)
133
+ button_layout.addWidget(self.save_button)
134
+ button_layout.addWidget(self.load_button)
135
+ layout.addLayout(button_layout)
136
+
137
+ # Create the state manager
138
+ self.state_manager = WidgetStateManager(self)
139
+
140
+ # Connect buttons
141
+ self.save_button.clicked.connect(lambda: self.state_manager.save_state())
142
+ self.load_button.clicked.connect(lambda: self.state_manager.load_state())
143
+
144
+
145
+ if __name__ == "__main__": # pragma: no cover:
146
+ import sys
147
+
148
+ app = QApplication(sys.argv)
149
+ w = ExampleApp()
150
+ w.show()
151
+ sys.exit(app.exec_())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 1.14.1
3
+ Version: 1.15.1
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
@@ -2,11 +2,11 @@
2
2
  .gitlab-ci.yml,sha256=CLlFGYRGKp4FxCPTkyF9p-7qx67KmbM9Yok9JQEU_Ls,8677
3
3
  .pylintrc,sha256=eeY8YwSI74oFfq6IYIbCqnx3Vk8ZncKaatv96n_Y8Rs,18544
4
4
  .readthedocs.yaml,sha256=aSOc277LqXcsTI6lgvm_JY80lMlr69GbPKgivua2cS0,603
5
- CHANGELOG.md,sha256=FFvwVqBUD_q2IiMmx4HdvyGB5NKM2DG0Tkcfcy_9reE,220975
5
+ CHANGELOG.md,sha256=PLH7TxQeADnLcLJvCxY8y4eWlHYlibflQw4N5agOy4Q,221574
6
6
  LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
7
- PKG-INFO,sha256=NtdzzO4MGjNTXBbNulluYh__nOLWcQX820P88EixW30,1339
7
+ PKG-INFO,sha256=eSWz36E758Ec4tYRAAudPgBBJ8yqKTijz2DjCULvlrk,1339
8
8
  README.md,sha256=Od69x-RS85Hph0-WwWACwal4yUd67XkEn4APEfHhHFw,2649
9
- pyproject.toml,sha256=a_6QuEHoaPGQEUAHvY24XelSuVDR_Df6wf0IAftbEvg,2596
9
+ pyproject.toml,sha256=vSsp1CG05ZIaEzBiIEtCyrXDUgHzET42kuJBJvDkpqE,2596
10
10
  .git_hooks/pre-commit,sha256=n3RofIZHJl8zfJJIUomcMyYGFi_rwq4CC19z0snz3FI,286
11
11
  .gitlab/issue_templates/bug_report_template.md,sha256=gAuyEwl7XlnebBrkiJ9AqffSNOywmr8vygUFWKTuQeI,386
12
12
  .gitlab/issue_templates/documentation_update_template.md,sha256=FHLdb3TS_D9aL4CYZCjyXSulbaW5mrN2CmwTaeLPbNw,860
@@ -49,7 +49,7 @@ bec_widgets/examples/plugin_example_pyside/tictactoetaskmenu.py,sha256=V6OVnBTS-
49
49
  bec_widgets/qt_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
50
  bec_widgets/qt_utils/collapsible_panel_manager.py,sha256=tvv77-9YTfYpsU6M_Le3bHR6wtANC83DEOrJ2Hhj6rs,14201
51
51
  bec_widgets/qt_utils/compact_popup.py,sha256=3yeb-GJ1PUla5Q_hT0XDKqvyIEH9yV_eGidf1t8Dbbw,10234
52
- bec_widgets/qt_utils/error_popups.py,sha256=Bm5-Gjl_vELFo12f8KgGRlk4hCCT9hfwGM7ggmvfCFs,9323
52
+ bec_widgets/qt_utils/error_popups.py,sha256=yVJ2sPOji5rUWEy7BgIEXUzvNQiw2njsaGhW6Hv_dx0,10828
53
53
  bec_widgets/qt_utils/palette_viewer.py,sha256=--B0x7aE7bniHIeuuLY_pH8yBDrTTXaE0IDrC_AM1mo,6326
54
54
  bec_widgets/qt_utils/redis_message_waiter.py,sha256=fvL_QgC0cTDv_FPJdRyp5AKjf401EJU4z3r38p47ydY,1745
55
55
  bec_widgets/qt_utils/round_frame.py,sha256=Ba_sTzYB_vYDepBBMPPqU8XDwKOAiU6ClZ3xUqiveK0,5734
@@ -83,6 +83,7 @@ bec_widgets/utils/thread_checker.py,sha256=rDNuA3X6KQyA7JPb67mccTg0z8YkInynLAENQ
83
83
  bec_widgets/utils/ui_loader.py,sha256=6z0Qvt99XWoIk_YMACShwQ1p7PbDh6uJ9wS6e2wZs0w,4878
84
84
  bec_widgets/utils/validator_delegate.py,sha256=Emj1WF6W8Ke1ruBWUfmHdVJpmOSPezuOt4zvQTay_44,442
85
85
  bec_widgets/utils/widget_io.py,sha256=R-ZYQyEVigHNH1AD4cNYmCV1DoO0XNidTZpiCSSND2c,15232
86
+ bec_widgets/utils/widget_state_manager.py,sha256=dtObn3xRY5mVTnkGTQDOPsAwkQ18pXJwYPgtCdRpKtc,4920
86
87
  bec_widgets/utils/yaml_dialog.py,sha256=T6UyGNGdmpXW74fa_7Nk6b99T5pp2Wvyw3AOauRc8T8,2407
87
88
  bec_widgets/utils/plugin_templates/plugin.template,sha256=DWtJdHpdsVtbiTTOniH3zBe5a40ztQ20o_-Hclyu38s,1266
88
89
  bec_widgets/utils/plugin_templates/register.template,sha256=XyL3OZPT_FTArLAM8tHd5qMqv2ZuAbJAZLsNNnHcagU,417
@@ -325,8 +326,8 @@ bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.py,sha256=Z
325
326
  bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button.pyproject,sha256=Lbi9zb6HNlIq14k6hlzR-oz6PIFShBuF7QxE6d87d64,34
326
327
  bec_widgets/widgets/utility/visual/dark_mode_button/dark_mode_button_plugin.py,sha256=CzChz2SSETYsR8-36meqWnsXCT-FIy_J_xeU5coWDY8,1350
327
328
  bec_widgets/widgets/utility/visual/dark_mode_button/register_dark_mode_button.py,sha256=rMpZ1CaoucwobgPj1FuKTnt07W82bV1GaSYdoqcdMb8,521
328
- bec_widgets-1.14.1.dist-info/METADATA,sha256=NtdzzO4MGjNTXBbNulluYh__nOLWcQX820P88EixW30,1339
329
- bec_widgets-1.14.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
330
- bec_widgets-1.14.1.dist-info/entry_points.txt,sha256=dItMzmwA1wizJ1Itx15qnfJ0ZzKVYFLVJ1voxT7K7D4,214
331
- bec_widgets-1.14.1.dist-info/licenses/LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
332
- bec_widgets-1.14.1.dist-info/RECORD,,
329
+ bec_widgets-1.15.1.dist-info/METADATA,sha256=eSWz36E758Ec4tYRAAudPgBBJ8yqKTijz2DjCULvlrk,1339
330
+ bec_widgets-1.15.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
331
+ bec_widgets-1.15.1.dist-info/entry_points.txt,sha256=dItMzmwA1wizJ1Itx15qnfJ0ZzKVYFLVJ1voxT7K7D4,214
332
+ bec_widgets-1.15.1.dist-info/licenses/LICENSE,sha256=YRKe85CBRyP7UpEAWwU8_qSIyuy5-l_9C-HKg5Qm8MQ,1511
333
+ bec_widgets-1.15.1.dist-info/RECORD,,
pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bec_widgets"
7
- version = "1.14.1"
7
+ version = "1.15.1"
8
8
  description = "BEC Widgets"
9
9
  requires-python = ">=3.10"
10
10
  classifiers = [