From 424bd662b279ce70c433d129339c5e6aa30f32fc Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Fri, 25 Apr 2025 10:53:50 +0200 Subject: [PATCH 1/2] refactor(omny_alignment_widget): improve type hints --- .../widgets/omny_alignment/omny_alignment.py | 160 ++++++++++-------- 1 file changed, 92 insertions(+), 68 deletions(-) diff --git a/csaxs_bec/bec_widgets/widgets/omny_alignment/omny_alignment.py b/csaxs_bec/bec_widgets/widgets/omny_alignment/omny_alignment.py index 089101f..7573b3d 100644 --- a/csaxs_bec/bec_widgets/widgets/omny_alignment/omny_alignment.py +++ b/csaxs_bec/bec_widgets/widgets/omny_alignment/omny_alignment.py @@ -1,33 +1,52 @@ +from __future__ import annotations - -from typing import TypedDict -from bec_widgets.utils.error_popups import SafeSlot import os -from bec_widgets.utils.bec_widget import BECWidget -from bec_widgets.utils.ui_loader import UILoader -from qtpy.QtWidgets import QWidget, QPushButton, QLineEdit, QLabel, QVBoxLayout -from bec_qthemes import material_icon +from typing import TYPE_CHECKING, TypedDict + from bec_lib.logger import bec_logger +from bec_qthemes import material_icon +from bec_widgets.utils.bec_widget import BECWidget +from bec_widgets.utils.error_popups import SafeSlot +from bec_widgets.utils.ui_loader import UILoader +from qtpy.QtWidgets import QVBoxLayout, QWidget -logger = bec_logger.logger +logger = bec_logger.logger + +if TYPE_CHECKING: + from bec_widgets.widgets.plots.image.image import Image + from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch + from qtpy.QtWidgets import QLineEdit, QPushButton -# class OmnyAlignmentUIComponents(TypedDict): -# moveRightButton: QPushButton -# moveLeftButton: QPushButton -# moveUpButton: QPushButton -# moveDownButton: QPushButton -# image: Image +class OmnyAlignmentUIComponents(TypedDict): + moveRightButton: QPushButton + moveLeftButton: QPushButton + moveUpButton: QPushButton + moveDownButton: QPushButton + confirmButton: QPushButton + image: Image + sampleLineEdit: QLineEdit + messageLineEdit: QLineEdit + liveViewSwitch: ToggleSwitch class OmnyAlignment(BECWidget, QWidget): - USER_ACCESS = ["enable_live_view", "enable_live_view.setter", "user_message", "user_message.setter","sample_name", "sample_name.setter", "enable_move_buttons", "enable_move_buttons.setter"] + USER_ACCESS = [ + "enable_live_view", + "enable_live_view.setter", + "user_message", + "user_message.setter", + "sample_name", + "sample_name.setter", + "enable_move_buttons", + "enable_move_buttons.setter", + ] PLUGIN = True ui_file = "./omny_alignment.ui" + components: OmnyAlignmentUIComponents def __init__(self, parent=None, **kwargs): super().__init__(parent=parent, **kwargs) - self._load_ui() def _load_ui(self): @@ -38,102 +57,107 @@ class OmnyAlignment(BECWidget, QWidget): self.setLayout(layout) icon_options = {"size": (16, 16), "convert_to_pixmap": False} - self.ui.moveRightButton.setText("") - self.ui.moveRightButton.setIcon( + + self.components: OmnyAlignmentUIComponents = { + "moveRightButton": self.ui.moveRightButton, + "moveLeftButton": self.ui.moveLeftButton, + "moveUpButton": self.ui.moveUpButton, + "moveDownButton": self.ui.moveDownButton, + "confirmButton": self.ui.confirmButton, + "image": self.ui.image, + "sampleLineEdit": self.ui.sampleLineEdit, + "messageLineEdit": self.ui.messageLineEdit, + "liveViewSwitch": self.ui.liveViewSwitch, + } + self.components["moveRightButton"].setText("") + self.components["moveRightButton"].setIcon( material_icon(icon_name="keyboard_arrow_right", **icon_options) ) - self.ui.moveLeftButton.setText("") - self.ui.moveLeftButton.setIcon( + self.components["moveLeftButton"].setText("") + self.components["moveLeftButton"].setIcon( material_icon(icon_name="keyboard_arrow_left", **icon_options) ) - - self.ui.moveUpButton.setText("") - self.ui.moveUpButton.setIcon( + self.components["moveUpButton"].setText("") + self.components["moveUpButton"].setIcon( material_icon(icon_name="keyboard_arrow_up", **icon_options) ) - - self.ui.moveDownButton.setText("") - self.ui.moveDownButton.setIcon( + self.components["moveDownButton"].setText("") + self.components["moveDownButton"].setIcon( material_icon(icon_name="keyboard_arrow_down", **icon_options) ) - self.ui.confirmButton.setText("OK") - - - self.ui.liveViewSwitch.enabled.connect(self.on_live_view_enabled) + self.components["confirmButton"].setText("OK") + self.components["liveViewSwitch"].enabled.connect(self.on_live_view_enabled) @property def enable_live_view(self): - return self.ui.liveViewSwitch.checked - - @enable_live_view.setter - def enable_live_view(self, enable:bool): - self.ui.liveViewSwitch.checked = enable + return self.components["liveViewSwitch"].checked + @enable_live_view.setter + def enable_live_view(self, enable: bool): + self.components["liveViewSwitch"].checked = enable @property def user_message(self): - return self.ui.messageLineEdit.text() - + return self.components["messageLineEdit"].text() + @user_message.setter - def user_message(self, message:str): - self.ui.messageLineEdit.setText(message) + def user_message(self, message: str): + self.components["messageLineEdit"].setText(message) @property def sample_name(self): - return self.ui.sampleLineEdit.text() - - @sample_name.setter - def sample_name(self, message:str): - self.ui.sampleLineEdit.setText(message) + return self.components["sampleLineEdit"].text() + @sample_name.setter + def sample_name(self, message: str): + self.components["sampleLineEdit"].setText(message) @SafeSlot(bool) - def on_live_view_enabled(self, enabled:bool): - from bec_widgets.widgets.plots.image.image import Image + def on_live_view_enabled(self, enabled: bool): logger.info(f"Live view is enabled: {enabled}") - image: Image = self.ui.image + image = self.components["image"] if enabled: image.image("cam200") return - + image.disconnect_monitor("cam200") - - @property def enable_move_buttons(self): - move_up:QPushButton = self.ui.moveUpButton - move_down:QPushButton = self.ui.moveDownButton - move_left:QPushButton = self.ui.moveLeftButton - move_right:QPushButton = self.ui.moveRightButton - return move_up.isEnabled() and move_down.isEnabled() and move_left.isEnabled() and move_right.isEnabled() - + move_up: QPushButton = self.components["moveUpButton"] + move_down: QPushButton = self.components["moveDownButton"] + move_left: QPushButton = self.components["moveLeftButton"] + move_right: QPushButton = self.components["moveRightButton"] + return ( + move_up.isEnabled() + and move_down.isEnabled() + and move_left.isEnabled() + and move_right.isEnabled() + ) + @enable_move_buttons.setter - def enable_move_buttons(self, enabled:bool): - move_up:QPushButton = self.ui.moveUpButton - move_down:QPushButton = self.ui.moveDownButton - move_left:QPushButton = self.ui.moveLeftButton - move_right:QPushButton = self.ui.moveRightButton + def enable_move_buttons(self, enabled: bool): + move_up: QPushButton = self.components["moveUpButton"] + move_down: QPushButton = self.components["moveDownButton"] + move_left: QPushButton = self.components["moveLeftButton"] + move_right: QPushButton = self.components["moveRightButton"] move_up.setEnabled(enabled) move_down.setEnabled(enabled) move_left.setEnabled(enabled) move_right.setEnabled(enabled) - - - - -if __name__ == "__main__": - from qtpy.QtWidgets import QApplication +if __name__ == "__main__": # pragma: no cover import sys + from qtpy.QtWidgets import QApplication + app = QApplication(sys.argv) widget = OmnyAlignment() widget.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) -- 2.49.1 From 34fc057cfd6695c015bb3996e8177b2e25488dd4 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Fri, 25 Apr 2025 11:34:35 +0200 Subject: [PATCH 2/2] test: add tests for omny alignment gui --- .../widgets/omny_alignment/omny_alignment.py | 2 +- pyproject.toml | 4 + tests/tests_bec_widgets/conftest.py | 83 +++++++++++++++++++ .../tests_bec_widgets/test_omny_alignment.py | 35 ++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 tests/tests_bec_widgets/conftest.py create mode 100644 tests/tests_bec_widgets/test_omny_alignment.py diff --git a/csaxs_bec/bec_widgets/widgets/omny_alignment/omny_alignment.py b/csaxs_bec/bec_widgets/widgets/omny_alignment/omny_alignment.py index 7573b3d..66f8418 100644 --- a/csaxs_bec/bec_widgets/widgets/omny_alignment/omny_alignment.py +++ b/csaxs_bec/bec_widgets/widgets/omny_alignment/omny_alignment.py @@ -12,7 +12,7 @@ from qtpy.QtWidgets import QVBoxLayout, QWidget logger = bec_logger.logger -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from bec_widgets.widgets.plots.image.image import Image from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch from qtpy.QtWidgets import QLineEdit, QPushButton diff --git a/pyproject.toml b/pyproject.toml index 8ddb4b0..f614b32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dev = [ "pytest", "pytest-random-order", "pytest-redis", + "pytest-qt", ] [project.entry-points."bec"] @@ -55,6 +56,9 @@ plugin_ipython_client_post = "csaxs_bec.bec_ipython_client.startup" [project.entry-points."bec.widgets.user_widgets"] plugin_widgets = "csaxs_bec.bec_widgets.widgets" +[project.entry-points."bec.widgets.auto_updates"] +plugin_widgets_update = "csaxs_bec.bec_widgets.auto_updates" + [tool.hatch.build.targets.wheel] include = ["*"] diff --git a/tests/tests_bec_widgets/conftest.py b/tests/tests_bec_widgets/conftest.py new file mode 100644 index 0000000..fd561bd --- /dev/null +++ b/tests/tests_bec_widgets/conftest.py @@ -0,0 +1,83 @@ +from unittest import mock + +import pytest +from bec_widgets.cli.rpc.rpc_register import RPCRegister +from bec_widgets.utils import bec_dispatcher as bec_dispatcher_module +from bec_widgets.utils import error_popups +from pytestqt.exceptions import TimeoutError as QtBotTimeoutError +from qtpy.QtWidgets import QApplication + + +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_makereport(item, call): + # execute all other hooks to obtain the report object + outcome = yield + rep = outcome.get_result() + + item.stash["failed"] = rep.failed + + +@pytest.fixture(autouse=True) +def qapplication(qtbot, request): # pylint: disable=unused-argument + yield + + # if the test failed, we don't want to check for open widgets as + # it simply pollutes the output + if request.node.stash._storage.get("failed"): + print("Test failed, skipping cleanup checks") + return + bec_dispatcher = bec_dispatcher_module.BECDispatcher() + bec_dispatcher.stop_cli_server() + + qapp = QApplication.instance() + qapp.processEvents() + if hasattr(qapp, "os_listener") and qapp.os_listener: + qapp.removeEventFilter(qapp.os_listener) + try: + qtbot.waitUntil(lambda: qapp.topLevelWidgets() == []) + except QtBotTimeoutError as exc: + raise TimeoutError(f"Failed to close all widgets: {qapp.topLevelWidgets()}") from exc + + +@pytest.fixture(autouse=True) +def rpc_register(): + yield RPCRegister() + RPCRegister.reset_singleton() + + +@pytest.fixture(autouse=True) +def bec_dispatcher(threads_check): # pylint: disable=unused-argument + bec_dispatcher = bec_dispatcher_module.BECDispatcher() + yield bec_dispatcher + bec_dispatcher.disconnect_all() + # clean BEC client + bec_dispatcher.client.shutdown() + # stop the cli server + bec_dispatcher.stop_cli_server() + # reinitialize singleton for next test + bec_dispatcher_module.BECDispatcher.reset_singleton() + + +@pytest.fixture(autouse=True) +def clean_singleton(): + error_popups._popup_utility_instance = None + + +def create_widget(qtbot, widget, *args, **kwargs): + """ + Create a widget and add it to the qtbot for testing. This is a helper function that + should be used in all tests that require a widget to be created. + + Args: + qtbot (fixture): pytest-qt fixture + widget (QWidget): widget class to be created + *args: positional arguments for the widget + **kwargs: keyword arguments for the widget + + Returns: + QWidget: the created widget + """ + widget = widget(*args, **kwargs) + qtbot.addWidget(widget) + qtbot.waitExposed(widget) + return widget diff --git a/tests/tests_bec_widgets/test_omny_alignment.py b/tests/tests_bec_widgets/test_omny_alignment.py new file mode 100644 index 0000000..407a8fe --- /dev/null +++ b/tests/tests_bec_widgets/test_omny_alignment.py @@ -0,0 +1,35 @@ +import pytest + +from csaxs_bec.bec_widgets.widgets.omny_alignment.omny_alignment import OmnyAlignment + + +@pytest.fixture +def omny_alignment(qtbot): + widget = OmnyAlignment() + qtbot.addWidget(widget) + qtbot.waitExposed(widget) + return widget + + +def test_omny_alignment_set_user_message(omny_alignment): + """ + Test the set_user_message method of the OmnyAlignment widget. + """ + # Set a message + message = "Test message" + omny_alignment.user_message = message + + # Check if the message is set correctly + assert omny_alignment.components["messageLineEdit"].text() == message + + +def test_omny_alignment_set_sample_name(omny_alignment): + """ + Test the set_sample_name method of the OmnyAlignment widget. + """ + # Set a sample name + sample_name = "Test sample" + omny_alignment.sample_name = sample_name + + # Check if the sample name is set correctly + assert omny_alignment.components["sampleLineEdit"].text() == sample_name -- 2.49.1