1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-15 13:10:54 +02:00

Compare commits

...

6 Commits

Author SHA1 Message Date
semantic-release
c684b6c230 2.9.2
Automatically generated by python-semantic-release
2025-05-30 13:03:46 +00:00
91126168b6 fix(log_panel): removed lambda callback method 2025-05-30 15:03:08 +02:00
7322cd194f fix: move log panel to bec connector and add rate limiter 2025-05-30 15:03:08 +02:00
d9dc60ee99 fix: logpanel error cycle 2025-05-30 15:03:08 +02:00
semantic-release
e4cd4891ad 2.9.1
Automatically generated by python-semantic-release
2025-05-30 11:27:23 +00:00
12f8c82eb5 fix: make registry update log message debug level 2025-05-30 13:26:40 +02:00
5 changed files with 95 additions and 39 deletions

View File

@@ -1,6 +1,28 @@
# CHANGELOG
## v2.9.2 (2025-05-30)
### Bug Fixes
- Logpanel error cycle
([`d9dc60e`](https://github.com/bec-project/bec_widgets/commit/d9dc60ee9974e2e6e6005378cc17ef088a4ded2c))
- Move log panel to bec connector and add rate limiter
([`7322cd1`](https://github.com/bec-project/bec_widgets/commit/7322cd194fcf7f56d41c86ecbcd97a5d8bd60c3e))
- **log_panel**: Removed lambda callback method
([`9112616`](https://github.com/bec-project/bec_widgets/commit/91126168b62f3e1623521ceb205dd854287cfef7))
## v2.9.1 (2025-05-30)
### Bug Fixes
- Make registry update log message debug level
([`12f8c82`](https://github.com/bec-project/bec_widgets/commit/12f8c82eb59ed6a7273b57126efe340bf37b65cc))
## v2.9.0 (2025-05-30)
### Bug Fixes

View File

@@ -195,7 +195,7 @@ class RPCServer:
return
self._broadcasted_data = data
logger.info(f"Broadcasting registry update: {data} for {self.gui_id}")
logger.debug(f"Broadcasting registry update: {data} for {self.gui_id}")
self.client.connector.xadd(
MessageEndpoints.gui_registry_state(self.gui_id),
msg_dict={"data": messages.GUIRegistryStateMessage(state=data)},

View File

@@ -11,12 +11,11 @@ from re import Pattern
from typing import TYPE_CHECKING, Literal
from bec_lib.client import BECClient
from bec_lib.connector import ConnectorBase
from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import LogLevel, bec_logger
from bec_lib.messages import LogMessage, StatusMessage
from PySide6.QtCore import QObject
from qtpy.QtCore import QDateTime, Qt, Signal
from pyqtgraph import SignalProxy
from qtpy.QtCore import QDateTime, QObject, Qt, Signal
from qtpy.QtGui import QFont
from qtpy.QtWidgets import (
QApplication,
@@ -35,6 +34,7 @@ from qtpy.QtWidgets import (
QWidget,
)
from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.utils.colors import get_theme_palette, set_theme
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.widgets.editors.text_box.text_box import TextBox
@@ -69,22 +69,22 @@ DEFAULT_LOG_COLORS = {
}
class BecLogsQueue(QObject):
class BecLogsQueue(BECConnector, QObject):
"""Manages getting logs from BEC Redis and formatting them for display"""
RPC = False
new_message = Signal()
def __init__(
self,
parent: QObject | None,
conn: ConnectorBase,
maxlen: int = 1000,
line_formatter: LineFormatter = noop_format,
**kwargs,
) -> None:
super().__init__(parent=parent)
super().__init__(parent=parent, **kwargs)
self._timestamp_start: QDateTime | None = None
self._timestamp_end: QDateTime | None = None
self._conn = conn
self._max_length = maxlen
self._data: deque[LogMessage] = deque([], self._max_length)
self._display_queue: deque[str] = deque([], self._max_length)
@@ -92,20 +92,26 @@ class BecLogsQueue(QObject):
self._search_query: Pattern | str | None = None
self._selected_services: set[str] | None = None
self._set_formatter_and_update_filter(line_formatter)
self._conn.register([MessageEndpoints.log()], None, self._process_incoming_log_msg)
# instance attribute still accessible after c++ object is deleted, so the callback can be unregistered
self.bec_dispatcher.connect_slot(self._process_incoming_log_msg, MessageEndpoints.log())
def unsub_from_redis(self):
def cleanup(self, *_):
"""Stop listening to the Redis log stream"""
self._conn.unregister([MessageEndpoints.log()], None, self._process_incoming_log_msg)
self.bec_dispatcher.disconnect_slot(
self._process_incoming_log_msg, [MessageEndpoints.log()]
)
def _process_incoming_log_msg(self, msg: dict):
@SafeSlot(verify_sender=True)
def _process_incoming_log_msg(self, msg: dict, _metadata: dict):
try:
_msg: LogMessage = msg["data"]
_msg = LogMessage(**msg)
self._data.append(_msg)
if self.filter is None or self.filter(_msg):
self._display_queue.append(self._line_formatter(_msg))
self.new_message.emit()
except Exception as e:
if "Internal C++ object (BecLogsQueue) already deleted." in e.args:
return
logger.warning(f"Error in LogPanel incoming message callback: {e}")
def _set_formatter_and_update_filter(self, line_formatter: LineFormatter = noop_format):
@@ -202,7 +208,7 @@ class BecLogsQueue(QObject):
"""Fetch all available messages from Redis"""
self._data = deque(
item["data"]
for item in self._conn.xread(
for item in self.bec_dispatcher.client.connector.xread(
MessageEndpoints.log().endpoint, from_start=True, count=self._max_length
)
)
@@ -396,7 +402,6 @@ class LogPanel(TextBox):
"""Displays a log panel"""
ICON_NAME = "terminal"
_new_messages = Signal()
service_list_update = Signal(dict, set)
def __init__(
@@ -407,17 +412,17 @@ class LogPanel(TextBox):
**kwargs,
):
"""Initialize the LogPanel widget."""
super().__init__(parent=parent, client=client, **kwargs)
super().__init__(parent=parent, client=client, config={"text": ""}, **kwargs)
self._update_colors()
self._service_status = service_status or BECServiceStatusMixin(self, client=self.client) # type: ignore
self._log_manager = BecLogsQueue(
parent,
self.client.connector, # type: ignore
line_formatter=partial(simple_color_format, colors=self._colors),
parent=self, line_formatter=partial(simple_color_format, colors=self._colors)
)
self._proxy_update = SignalProxy(
self._log_manager.new_message, rateLimit=1, slot=self._on_append
)
self._log_manager.new_message.connect(self._new_messages)
self.toolbar = LogPanelToolbar(parent=parent)
self.toolbar = LogPanelToolbar(parent=self)
self.toolbar_area = QScrollArea()
self.toolbar_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.toolbar_area.setSizeAdjustPolicy(QScrollArea.SizeAdjustPolicy.AdjustToContents)
@@ -431,7 +436,6 @@ class LogPanel(TextBox):
self.toolbar.search_textbox.returnPressed.connect(self._on_re_update)
self.toolbar.regex_enabled.checkStateChanged.connect(self._on_re_update)
self.toolbar.filter_level_dropdown.currentTextChanged.connect(self._set_level_filter)
self._new_messages.connect(self._on_append)
self.toolbar.timerange_button.clicked.connect(self._choose_datetime)
self._service_status.services_update.connect(self._update_service_list)
@@ -483,10 +487,10 @@ class LogPanel(TextBox):
self.set_html_text(self._log_manager.display_all())
self._cursor_to_end()
@SafeSlot()
def _on_append(self):
self._cursor_to_end()
@SafeSlot(verify_sender=True)
def _on_append(self, *_):
self.text_box_text_edit.insertHtml(self._log_manager.format_new())
self._cursor_to_end()
@SafeSlot()
def _on_clear(self):
@@ -529,9 +533,8 @@ class LogPanel(TextBox):
def cleanup(self):
self._service_status.cleanup()
self._log_manager.unsub_from_redis()
self._log_manager.new_message.disconnect(self._new_messages)
self._new_messages.disconnect(self._on_append)
self._log_manager.cleanup()
self._log_manager.deleteLater()
super().cleanup()

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "bec_widgets"
version = "2.9.0"
version = "2.9.2"
description = "BEC Widgets"
requires-python = ">=3.10"
classifiers = [

View File

@@ -4,11 +4,12 @@
# pylint: disable=protected-access
from collections import deque
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
import pytest
from bec_lib.messages import LogMessage
from qtpy.QtCore import QDateTime, Qt, Signal # type: ignore
from bec_lib.redis_connector import StreamMessage
from qtpy.QtCore import QDateTime
from bec_widgets.widgets.utility.logpanel._util import (
log_time,
@@ -65,7 +66,6 @@ def log_panel(qtbot, mocked_client: MagicMock):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.cleanup()
def test_log_panel_init(log_panel: LogPanel):
@@ -97,14 +97,13 @@ def test_logpanel_output(qtbot, log_panel: LogPanel):
return len(log_panel._log_manager._display_queue) == 0
next_text = "datetime | error | test log message"
msg = LogMessage(
metadata={},
log_type="error",
log_msg={"text": next_text, "record": {}, "service_name": "ScanServer"},
)
log_panel._log_manager._process_incoming_log_msg(
{
"data": LogMessage(
metadata={},
log_type="error",
log_msg={"text": next_text, "record": {}, "service_name": "ScanServer"},
)
}
msg.content, msg.metadata, _override_slot_params={"verify_sender": False}
)
qtbot.waitUntil(display_queue_empty, timeout=5000)
@@ -136,3 +135,35 @@ def test_timestamp_filter(log_panel: LogPanel):
assert not filter_(TEST_LOG_MESSAGES[0])
assert filter_(TEST_LOG_MESSAGES[1])
assert not filter_(TEST_LOG_MESSAGES[2])
def test_error_handling_in_callback(log_panel: LogPanel):
log_panel._log_manager.new_message = MagicMock()
with patch("bec_widgets.widgets.utility.logpanel.logpanel.logger") as logger:
# generally errors should be logged
log_panel._log_manager.new_message.emit = MagicMock(
side_effect=ValueError("Something went wrong")
)
msg = LogMessage(
metadata={},
log_type="debug",
log_msg={
"text": "datetime | debug | test log message",
"record": {"time": {"timestamp": 123456789.000}},
"service_name": "ScanServer",
},
)
log_panel._log_manager._process_incoming_log_msg(
msg.content, msg.metadata, _override_slot_params={"verify_sender": False}
)
logger.warning.assert_called_once()
# this specific error should be ignored and not relogged
log_panel._log_manager.new_message.emit = MagicMock(
side_effect=RuntimeError("Internal C++ object (BecLogsQueue) already deleted.")
)
log_panel._log_manager._process_incoming_log_msg(
msg.content, msg.metadata, _override_slot_params={"verify_sender": False}
)
logger.warning.assert_called_once()