bec-widgets 0.66.1__py3-none-any.whl → 0.68.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 CHANGED
@@ -1,5 +1,47 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v0.68.0 (2024-06-21)
4
+
5
+ ### Feature
6
+
7
+ * feat: properly handle SIGINT (ctrl-c) in BEC GUI server -> calls qapplication.quit() ([`3644f34`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3644f344da2df674bc0d5740c376a86b9d0dfe95))
8
+
9
+ * feat: bec-gui-server: redirect stdout and stderr (if any) as proper debug and error log entries ([`d1266a1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d1266a1ce148ff89557a039e3a182a87a3948f49))
10
+
11
+ * feat: add logger for BEC GUI server ([`630616e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/630616ec729f60aa0b4d17a9e0379f9c6198eb96))
12
+
13
+ ### Fix
14
+
15
+ * fix: ignore GUI server output (any output will go to log file)
16
+
17
+ If a logger is given to log `_start_log_process`, the server stdout and
18
+ stderr streams will be redirected as log entries with levels DEBUG or ERROR
19
+ in their parent process ([`ce37416`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ce374163cab87a92847409051739777bc505a77b))
20
+
21
+ * fix: do not create 'BECClient' logger when instantiating BECDispatcher ([`f7d0b07`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f7d0b0768ace42a33e2556bb33611d4f02e5a6d9))
22
+
23
+ ## v0.67.0 (2024-06-21)
24
+
25
+ ### Documentation
26
+
27
+ * docs: add widget to documentation ([`6fa1c06`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6fa1c06053131dabd084bb3cf13c853b5d3ce833))
28
+
29
+ ### Feature
30
+
31
+ * feat: introduce BECStatusBox Widget ([`443b6c1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/443b6c1d7b02c772fda02e2d1eefd5bd40249e0c))
32
+
33
+ ### Refactor
34
+
35
+ * refactor: Change inheritance to QTreeWidget from QWidget ([`d2f2b20`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d2f2b206bb0eab60b8a9b0d0ac60a6b7887fa6fb))
36
+
37
+ ### Test
38
+
39
+ * test: add test suite for bec_status_box and status_item ([`5d4ca81`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5d4ca816cdedec4c88aba9eb326f85392504ea1c))
40
+
41
+ ### Unknown
42
+
43
+ * Update file requirements.txt ([`505a5ec`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/505a5ec8334ff4422913b3a7b79d39bcb42ad535))
44
+
3
45
  ## v0.66.1 (2024-06-20)
4
46
 
5
47
  ### Fix
@@ -133,46 +175,6 @@ This reverts commit abc6caa2d0b6141dfbe1f3d025f78ae14deddcb3 ([`fe04dd8`](https:
133
175
 
134
176
  ## v0.62.0 (2024-06-12)
135
177
 
136
- ### Feature
137
-
138
- * feat: implement non-polling, interruptible waiting of gui instruction response with timeout ([`abc6caa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/abc6caa2d0b6141dfbe1f3d025f78ae14deddcb3))
139
-
140
178
  ### Unknown
141
179
 
142
180
  * doc: add documentation about creating custom GUI applications embedding BEC Widgets ([`17a0068`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/17a00687579f5efab1990cd83862ec0e78198633))
143
-
144
- ## v0.61.0 (2024-06-12)
145
-
146
- ### Feature
147
-
148
- * feat(widgets/stop_button): General stop button added ([`61ba08d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/61ba08d0b8df9f48f5c54c7c2b4e6d395206e7e6))
149
-
150
- ### Refactor
151
-
152
- * refactor: improve labe of auto_update script ([`40b5688`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/40b568815893cd41af3531bb2e647ca1e2e315f4))
153
-
154
- ## v0.60.0 (2024-06-08)
155
-
156
- ### Ci
157
-
158
- * ci: added git fetch for target branch ([`fc4f4f8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fc4f4f81ad1be99cf5112f2188a46c5bed2679ee))
159
-
160
- * ci: fixed pylint-check ([`6b1d582`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6b1d5827d6599f06a3acd316060a8d25f0686d54))
161
-
162
- ### Feature
163
-
164
- * feat: added isort to bw-generate-cli ([`f0391f5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f0391f59c9eb0a51b693fccfe2e399e869d35dda))
165
-
166
- ### Fix
167
-
168
- * fix: removed BECConnector from rpc client interface ([`6428e38`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6428e38ab94c15a2c904e75cc6404bb6d0394e04))
169
-
170
- ### Refactor
171
-
172
- * refactor: minor cleanup ([`3adf6cf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3adf6cfd586355c8b8ce7fdc9722f868e22287c5))
173
-
174
- * refactor: disabled pylint for auto-gen client ([`b15816c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b15816ca9fd3e4ae87cca5fcfe029b4dfca570ca))
175
-
176
- ### Test
177
-
178
- * test: added missing pylint statement to header ([`f662985`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f6629852ebc2b4ee239fa560cc310a5ae2627cf7))
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: bec_widgets
3
- Version: 0.66.1
3
+ Version: 0.68.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
bec_widgets/cli/client.py CHANGED
@@ -13,6 +13,7 @@ class Widgets(str, enum.Enum):
13
13
  Enum for the available widgets.
14
14
  """
15
15
 
16
+ BECStatusBox = "BECStatusBox"
16
17
  BECDock = "BECDock"
17
18
  BECDockArea = "BECDockArea"
18
19
  BECFigure = "BECFigure"
@@ -1388,6 +1389,24 @@ class BECPlotBase(RPCBase):
1388
1389
  """
1389
1390
 
1390
1391
 
1392
+ class BECStatusBox(RPCBase):
1393
+ @property
1394
+ @rpc_call
1395
+ def config_dict(self) -> "dict":
1396
+ """
1397
+ Get the configuration of the widget.
1398
+
1399
+ Returns:
1400
+ dict: The configuration of the widget.
1401
+ """
1402
+
1403
+ @rpc_call
1404
+ def get_all_rpc(self) -> "dict":
1405
+ """
1406
+ Get all registered RPC objects.
1407
+ """
1408
+
1409
+
1391
1410
  class BECWaveform(RPCBase):
1392
1411
  @property
1393
1412
  @rpc_call
@@ -1649,6 +1668,60 @@ class BECWaveform(RPCBase):
1649
1668
  """
1650
1669
 
1651
1670
 
1671
+ class DeviceComboBox(RPCBase):
1672
+ @property
1673
+ @rpc_call
1674
+ def config_dict(self) -> "dict":
1675
+ """
1676
+ Get the configuration of the widget.
1677
+
1678
+ Returns:
1679
+ dict: The configuration of the widget.
1680
+ """
1681
+
1682
+ @rpc_call
1683
+ def get_all_rpc(self) -> "dict":
1684
+ """
1685
+ Get all registered RPC objects.
1686
+ """
1687
+
1688
+
1689
+ class DeviceInputBase(RPCBase):
1690
+ @property
1691
+ @rpc_call
1692
+ def config_dict(self) -> "dict":
1693
+ """
1694
+ Get the configuration of the widget.
1695
+
1696
+ Returns:
1697
+ dict: The configuration of the widget.
1698
+ """
1699
+
1700
+ @rpc_call
1701
+ def get_all_rpc(self) -> "dict":
1702
+ """
1703
+ Get all registered RPC objects.
1704
+ """
1705
+
1706
+
1707
+ class DeviceLineEdit(RPCBase):
1708
+ @property
1709
+ @rpc_call
1710
+ def config_dict(self) -> "dict":
1711
+ """
1712
+ Get the configuration of the widget.
1713
+
1714
+ Returns:
1715
+ dict: The configuration of the widget.
1716
+ """
1717
+
1718
+ @rpc_call
1719
+ def get_all_rpc(self) -> "dict":
1720
+ """
1721
+ Get all registered RPC objects.
1722
+ """
1723
+
1724
+
1652
1725
  class Ring(RPCBase):
1653
1726
  @rpc_call
1654
1727
  def get_all_rpc(self) -> "dict":
@@ -1950,7 +2023,7 @@ class TextBox(RPCBase):
1950
2023
  @rpc_call
1951
2024
  def set_color(self, background_color: str, font_color: str) -> None:
1952
2025
  """
1953
- Set the background color of the Widget.
2026
+ Set the background color of the widget.
1954
2027
 
1955
2028
  Args:
1956
2029
  background_color (str): The color to set the background in HEX.
@@ -1960,13 +2033,19 @@ class TextBox(RPCBase):
1960
2033
  @rpc_call
1961
2034
  def set_text(self, text: str) -> None:
1962
2035
  """
1963
- Set the text of the Widget
2036
+ Set the text of the widget.
2037
+
2038
+ Args:
2039
+ text (str): The text to set.
1964
2040
  """
1965
2041
 
1966
2042
  @rpc_call
1967
2043
  def set_font_size(self, size: int) -> None:
1968
2044
  """
1969
- Set the font size of the text in the Widget.
2045
+ Set the font size of the text in the widget.
2046
+
2047
+ Args:
2048
+ size (int): The font size to set.
1970
2049
  """
1971
2050
 
1972
2051
 
@@ -13,6 +13,7 @@ from functools import wraps
13
13
  from typing import TYPE_CHECKING
14
14
 
15
15
  from bec_lib.endpoints import MessageEndpoints
16
+ from bec_lib.logger import bec_logger
16
17
  from bec_lib.utils.import_utils import isinstance_based_on_class_name, lazy_import, lazy_import_from
17
18
  from qtpy.QtCore import QEventLoop, QSocketNotifier, QTimer
18
19
 
@@ -31,6 +32,8 @@ messages = lazy_import("bec_lib.messages")
31
32
  MessageObject = lazy_import_from("bec_lib.connector", ("MessageObject",))
32
33
  BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
33
34
 
35
+ logger = bec_logger.logger
36
+
34
37
 
35
38
  def rpc_call(func):
36
39
  """
@@ -63,45 +66,64 @@ def rpc_call(func):
63
66
  return wrapper
64
67
 
65
68
 
66
- def _get_output(process) -> None:
69
+ def _get_output(process, logger) -> None:
70
+ log_func = {process.stdout: logger.debug, process.stderr: logger.error}
71
+ stream_buffer = {process.stdout: [], process.stderr: []}
67
72
  try:
68
73
  os.set_blocking(process.stdout.fileno(), False)
69
74
  os.set_blocking(process.stderr.fileno(), False)
70
75
  while process.poll() is None:
71
76
  readylist, _, _ = select.select([process.stdout, process.stderr], [], [], 1)
72
- if process.stdout in readylist:
73
- output = process.stdout.read(1024)
77
+ for stream in (process.stdout, process.stderr):
78
+ buf = stream_buffer[stream]
79
+ if stream in readylist:
80
+ buf.append(stream.read(4096))
81
+ output, _, remaining = "".join(buf).rpartition("\n")
74
82
  if output:
75
- print(output, end="")
76
- if process.stderr in readylist:
77
- error_output = process.stderr.read(1024)
78
- if error_output:
79
- print(error_output, end="", file=sys.stderr)
83
+ log_func[stream](output)
84
+ buf.clear()
85
+ buf.append(remaining)
80
86
  except Exception as e:
81
87
  print(f"Error reading process output: {str(e)}")
82
88
 
83
89
 
84
- def _start_plot_process(gui_id, gui_class, config) -> None:
90
+ def _start_plot_process(gui_id, gui_class, config, logger=None) -> None:
85
91
  """
86
92
  Start the plot in a new process.
93
+
94
+ Logger must be a logger object with "debug" and "error" functions,
95
+ or it can be left to "None" as default. None means output from the
96
+ process will not be captured.
87
97
  """
88
98
  # pylint: disable=subprocess-run-check
89
- command = [
90
- "bec-gui-server",
91
- "--id",
92
- gui_id,
93
- "--config",
94
- config,
95
- "--gui_class",
96
- gui_class.__name__,
97
- ]
99
+ command = ["bec-gui-server", "--id", gui_id, "--gui_class", gui_class.__name__]
100
+ if config:
101
+ command.extend(["--config", config])
102
+
98
103
  env_dict = os.environ.copy()
99
104
  env_dict["PYTHONUNBUFFERED"] = "1"
105
+ if logger is None:
106
+ stdout_redirect = subprocess.DEVNULL
107
+ stderr_redirect = subprocess.DEVNULL
108
+ else:
109
+ stdout_redirect = subprocess.PIPE
110
+ stderr_redirect = subprocess.PIPE
111
+
100
112
  process = subprocess.Popen(
101
- command, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env_dict
113
+ command,
114
+ text=True,
115
+ start_new_session=True,
116
+ stdout=stdout_redirect,
117
+ stderr=stderr_redirect,
118
+ env=env_dict,
102
119
  )
103
- process_output_processing_thread = threading.Thread(target=_get_output, args=(process,))
104
- process_output_processing_thread.start()
120
+ if logger is None:
121
+ process_output_processing_thread = None
122
+ else:
123
+ process_output_processing_thread = threading.Thread(
124
+ target=_get_output, args=(process, logger)
125
+ )
126
+ process_output_processing_thread.start()
105
127
  return process, process_output_processing_thread
106
128
 
107
129
 
@@ -113,7 +135,6 @@ class BECGuiClientMixin:
113
135
  self.auto_updates = self._get_update_script()
114
136
  self._target_endpoint = MessageEndpoints.scan_status()
115
137
  self._selected_device = None
116
- self.stderr_output = []
117
138
 
118
139
  def _get_update_script(self) -> AutoUpdates | None:
119
140
  eps = imd.entry_points(group="bec.widgets.auto_updates")
@@ -165,7 +186,7 @@ class BECGuiClientMixin:
165
186
  if self._process is None or self._process.poll() is not None:
166
187
  self._start_update_script()
167
188
  self._process, self._process_output_processing_thread = _start_plot_process(
168
- self._gui_id, self.__class__, self._client._service_config.redis
189
+ self._gui_id, self.__class__, self._client._service_config.config_path
169
190
  )
170
191
  while not self.gui_is_alive():
171
192
  print("Waiting for GUI to start...")
@@ -185,19 +206,10 @@ class BECGuiClientMixin:
185
206
  self._client.shutdown()
186
207
  if self._process:
187
208
  self._process.terminate()
188
- self._process_output_processing_thread.join()
209
+ if self._process_output_processing_thread:
210
+ self._process_output_processing_thread.join()
189
211
  self._process = None
190
212
 
191
- def print_log(self) -> None:
192
- """
193
- Print the log of the plot process.
194
- """
195
- if self._process is None:
196
- return
197
- print("".join(self.stderr_output))
198
- # Flush list
199
- self.stderr_output.clear()
200
-
201
213
 
202
214
  class RPCResponseTimeoutError(Exception):
203
215
  """Exception raised when an RPC response is not received within the expected time."""
bec_widgets/cli/server.py CHANGED
@@ -1,17 +1,24 @@
1
1
  import inspect
2
+ import signal
3
+ import sys
4
+ from contextlib import redirect_stderr, redirect_stdout
2
5
  from typing import Union
3
6
 
4
7
  from bec_lib.endpoints import MessageEndpoints
8
+ from bec_lib.logger import bec_logger
9
+ from bec_lib.service_config import ServiceConfig
5
10
  from bec_lib.utils.import_utils import lazy_import
6
11
  from qtpy.QtCore import QTimer
7
12
 
8
13
  from bec_widgets.cli.rpc_register import RPCRegister
9
14
  from bec_widgets.utils import BECDispatcher
10
15
  from bec_widgets.utils.bec_connector import BECConnector
16
+ from bec_widgets.utils.bec_dispatcher import QtRedisConnector
11
17
  from bec_widgets.widgets.dock.dock_area import BECDockArea
12
18
  from bec_widgets.widgets.figure import BECFigure
13
19
 
14
20
  messages = lazy_import("bec_lib.messages")
21
+ logger = bec_logger.logger
15
22
 
16
23
 
17
24
  class BECWidgetsCLIServer:
@@ -114,6 +121,23 @@ class BECWidgetsCLIServer:
114
121
  self.client.shutdown()
115
122
 
116
123
 
124
+ class SimpleFileLikeFromLogOutputFunc:
125
+ def __init__(self, log_func):
126
+ self._log_func = log_func
127
+
128
+ def write(self, buffer):
129
+ for line in buffer.rstrip().splitlines():
130
+ line = line.rstrip()
131
+ if line:
132
+ self._log_func(line)
133
+
134
+ def flush(self):
135
+ return
136
+
137
+ def close(self):
138
+ return
139
+
140
+
117
141
  def main():
118
142
  import argparse
119
143
  import os
@@ -125,16 +149,6 @@ def main():
125
149
 
126
150
  import bec_widgets
127
151
 
128
- app = QApplication(sys.argv)
129
- app.setApplicationName("BEC Figure")
130
- module_path = os.path.dirname(bec_widgets.__file__)
131
- icon = QIcon()
132
- icon.addFile(os.path.join(module_path, "assets", "bec_widgets_icon.png"), size=QSize(48, 48))
133
- app.setWindowIcon(icon)
134
-
135
- win = QMainWindow()
136
- win.setWindowTitle("BEC Widgets")
137
-
138
152
  parser = argparse.ArgumentParser(description="BEC Widgets CLI Server")
139
153
  parser.add_argument("--id", type=str, help="The id of the server")
140
154
  parser.add_argument(
@@ -142,7 +156,7 @@ def main():
142
156
  type=str,
143
157
  help="Name of the gui class to be rendered. Possible values: \n- BECFigure\n- BECDockArea",
144
158
  )
145
- parser.add_argument("--config", type=str, help="Config to connect to redis.")
159
+ parser.add_argument("--config", type=str, help="Config file")
146
160
 
147
161
  args = parser.parse_args()
148
162
 
@@ -157,15 +171,44 @@ def main():
157
171
  )
158
172
  gui_class = BECFigure
159
173
 
160
- server = BECWidgetsCLIServer(gui_id=args.id, config=args.config, gui_class=gui_class)
174
+ with redirect_stdout(SimpleFileLikeFromLogOutputFunc(logger.debug)):
175
+ with redirect_stderr(SimpleFileLikeFromLogOutputFunc(logger.error)):
176
+ app = QApplication(sys.argv)
177
+ app.setApplicationName("BEC Figure")
178
+ module_path = os.path.dirname(bec_widgets.__file__)
179
+ icon = QIcon()
180
+ icon.addFile(
181
+ os.path.join(module_path, "assets", "bec_widgets_icon.png"), size=QSize(48, 48)
182
+ )
183
+ app.setWindowIcon(icon)
184
+
185
+ win = QMainWindow()
186
+ win.setWindowTitle("BEC Widgets")
187
+
188
+ service_config = ServiceConfig(args.config)
189
+ bec_logger.configure(
190
+ service_config.redis,
191
+ QtRedisConnector,
192
+ service_name="BECWidgetsCLIServer",
193
+ service_config=service_config.service_config,
194
+ )
195
+ server = BECWidgetsCLIServer(gui_id=args.id, config=service_config, gui_class=gui_class)
196
+
197
+ gui = server.gui
198
+ win.setCentralWidget(gui)
199
+ win.resize(800, 600)
200
+ win.show()
201
+
202
+ app.aboutToQuit.connect(server.shutdown)
203
+
204
+ def sigint_handler(*args):
205
+ # display message, for people to let it terminate gracefully
206
+ print("Caught SIGINT, exiting")
207
+ app.quit()
161
208
 
162
- gui = server.gui
163
- win.setCentralWidget(gui)
164
- win.resize(800, 600)
165
- win.show()
209
+ signal.signal(signal.SIGINT, sigint_handler)
166
210
 
167
- app.aboutToQuit.connect(server.shutdown)
168
- sys.exit(app.exec())
211
+ sys.exit(app.exec())
169
212
 
170
213
 
171
214
  if __name__ == "__main__": # pragma: no cover
@@ -66,6 +66,11 @@ class QtRedisConnector(RedisConnector):
66
66
  cb(msg.content, msg.metadata)
67
67
 
68
68
 
69
+ class BECClientWithoutLoggerInit(BECClient):
70
+ def _initialize_logger(self):
71
+ return
72
+
73
+
69
74
  class BECDispatcher:
70
75
  """Utility class to keep track of slots connected to a particular redis connector"""
71
76
 
@@ -79,7 +84,7 @@ class BECDispatcher:
79
84
  cls._initialized = False
80
85
  return cls._instance
81
86
 
82
- def __init__(self, client=None, config: str = None):
87
+ def __init__(self, client=None, config: str | ServiceConfig = None):
83
88
  if self._initialized:
84
89
  return
85
90
 
@@ -91,13 +96,16 @@ class BECDispatcher:
91
96
 
92
97
  if self.client is None:
93
98
  if config is not None:
94
- host, port = config.split(":")
95
- redis_config = {"host": host, "port": port}
96
- self.client = BECClient(
97
- config=ServiceConfig(redis=redis_config), connector_cls=QtRedisConnector
99
+ if not isinstance(config, ServiceConfig):
100
+ # config is supposed to be a path
101
+ config = ServiceConfig(config)
102
+ self.client = BECClientWithoutLoggerInit(
103
+ config=config, connector_cls=QtRedisConnector
98
104
  ) # , forced=True)
99
105
  else:
100
- self.client = BECClient(connector_cls=QtRedisConnector) # , forced=True)
106
+ self.client = BECClientWithoutLoggerInit(
107
+ connector_cls=QtRedisConnector
108
+ ) # , forced=True)
101
109
  else:
102
110
  if self.client.started:
103
111
  # have to reinitialize client to use proper connector
File without changes