From 8ea8957d8d5d392347b7375274ac64e290b447ae Mon Sep 17 00:00:00 2001 From: Malik Irain Date: Mon, 27 Oct 2025 11:29:20 +0100 Subject: [PATCH 01/11] Modified ROIMixin to avoid multiple initialization of QObject --- src/pymodaq_gui/plotting/items/roi.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/pymodaq_gui/plotting/items/roi.py b/src/pymodaq_gui/plotting/items/roi.py index 75b67ce7..def3325a 100644 --- a/src/pymodaq_gui/plotting/items/roi.py +++ b/src/pymodaq_gui/plotting/items/roi.py @@ -100,20 +100,23 @@ def get_descriptors_from_dimensionality(cls, dim: DataDim): return descriptors -class ROIMixin(QtCore.QObject): +class ROIMixin: index_signal = Signal(int) def __init__(self, index=0, name='roi', compute=True): - super().__init__() self.name = name self.index = index self._compute = compute self.menu = None + self.signalBlocker = None + self._clipboard = None + def init_qt(self): self.signalBlocker = QSignalBlocker(self) self.signalBlocker.unblock() self._clipboard = QtGui.QGuiApplication.clipboard() + def emit_index_signal(self): self.index_signal.emit(self.index) @@ -180,9 +183,11 @@ class ROI(pgROI, ROIMixin, ROIBase): sigRemoveRequested = Signal(object) def __init__(self, *args, index=0, name='roi', compute=True, **kwargs): - ROIMixin.__init__(self, index=index, name=name, compute=compute) pgROI.__init__(self, *args, **kwargs) ROIBase.__init__(self) + ROIMixin.__init__(self, index=index, name=name, compute=compute) + + self.init_qt() def getMenu(self): if self.menu is None: @@ -282,9 +287,11 @@ class LinearROI(pgLinearROI, ROIMixin, ROIBase): DESCRIPTOR = 'LinearROI' def __init__(self, index=0, pos=[0, 10], name = 'roi', compute=True, **kwargs): - ROIMixin.__init__(self, index=index, name=name, compute=compute) pgLinearROI.__init__(self, values=pos, **kwargs) ROIBase.__init__(self) + ROIMixin.__init__(self, index=index, name=name, compute=compute) + + self.init_qt() def getMenu(self): if self.menu is None: @@ -343,7 +350,6 @@ def setPen(self, color): def color(self): return self.brush.color() - @ROIFactory.register() class EllipseROI(ROI): """ From 9513e4e99159226b47dd9013b11adc3e897fd167 Mon Sep 17 00:00:00 2001 From: Malik Irain Date: Mon, 27 Oct 2025 11:33:03 +0100 Subject: [PATCH 02/11] fix: QGraphicsItem.type returns an int and not the python type --- tests/plotting_test/data_viewers_test/viewer2D_test.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/plotting_test/data_viewers_test/viewer2D_test.py b/tests/plotting_test/data_viewers_test/viewer2D_test.py index ffea6a2e..63cc0e84 100644 --- a/tests/plotting_test/data_viewers_test/viewer2D_test.py +++ b/tests/plotting_test/data_viewers_test/viewer2D_test.py @@ -14,6 +14,8 @@ from pytest import fixture, approx import numpy as np import pyqtgraph as pg + +from pymodaq_gui.plotting.items.roi import RectROI, EllipseROI from pymodaq_gui.plotting.utils.plot_utils import RoiInfo, Point from pyqtgraph import mkPen @@ -380,11 +382,12 @@ def test_add_roi(self, init_viewer2D): prog.show_data(data) index_roi, roi, roi_type = create_one_roi(prog, qtbot, roitype='RectROI') - assert roi.type() == 'RectROI' + + assert isinstance(roi, RectROI) assert roi.index == 0 index_roi, roi, roi_type = create_one_roi(prog, qtbot, roitype='EllipseROI') - assert roi.type() == 'EllipseROI' + assert isinstance(roi, EllipseROI) assert roi.index == 1 def test_remove_roi(self, init_viewer2D): From 05ec7acaccadab20a699e8689996aa7a993b7f5c Mon Sep 17 00:00:00 2001 From: Malik Irain Date: Mon, 27 Oct 2025 13:41:29 +0100 Subject: [PATCH 03/11] adapted workflow --- .github/workflows/tests.yml | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index eb68f1e6..1265abde 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -57,7 +57,11 @@ jobs: if: runner.os == 'Linux' run: | sudo apt update - sudo apt install -y yaru-theme-icon libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-cursor0 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils libgl1 libegl1 + sudo apt install -y xvfb mesa-utils libopengl0 yaru-theme-icon libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 \ + libxcb-keysyms1 libxcb-cursor0 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils \ + libx11-xcb1 libxrender1 libxext6 libxi6 libxrandr2 libxinerama1 libsm6 libegl1 libgl1-mesa-dri libglx-mesa0 \ + libglu1-mesa libfontconfig1 libfreetype6 libxcb-shape0 libxcb-shm0 libxcb-sync1 libxcomposite1 libxcursor1 \ + libxdamage1 libxtst6 libxcb1 export QT_DEBUG_PLUGINS=1 echo "XDG_CURRENT_DESKTOP=Unity" >> $GITHUB_ENV @@ -66,19 +70,35 @@ jobs: sudo mkdir -p /etc/.pymodaq sudo chmod uo+rw /etc/.pymodaq - - name: Exporting debug variables (Windows) - if: runner.os == 'Windows' - run: | - set QT_DEBUG_PLUGINS=1 + - name: Linting with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=docs - name: Tests with ${{ matrix.os }} ${{ matrix.python-version }} ${{ matrix.qt-backend}} + shell: bash id: tests run: | mkdir coverage - pytest -vv --cov=pymodaq_gui -n 1 + if [[ "${{ runner.os }}" == "Linux" ]]; then + export QT_DEBUG_PLUGINS=1 + export QT_OPENGL=software + export QT_QPA_PLATFORM=offscreen + export LIBGL_ALWAYS_SOFTWARE=1 + export MESA_LOADER_DRIVER_OVERRIDE=softpipe + export QT_XCB_GL_INTEGRATION=none + + xvfb-run --server-args='-screen 0 1024x768x24' python -m pytest -vv --cov=pymodaq_gui + else + set QT_DEBUG_PLUGINS=1 + set QT_OPENGL=software + set QT_QPA_PLATFORM=offscreen + set LIBGL_ALWAYS_SOFTWARE=1 + set MESA_LOADER_DRIVER_OVERRIDE=softpipe + set QT_XCB_GL_INTEGRATION=none + + pytest -vv --cov=pymodaq_gui + fi mv .coverage coverage/coverage_${{ matrix.os }}_${{ matrix.python-version }}_${{ matrix.qt-backend }} - name: Upload coverage artifact uses: actions/upload-artifact@v4.6.2 From 08c7f615ed386863efab8d5c5c129f32dae60d49 Mon Sep 17 00:00:00 2001 From: Malik Irain Date: Thu, 30 Oct 2025 11:17:47 +0100 Subject: [PATCH 04/11] set SearchLineEdit timer parent --- src/pymodaq_gui/utils/widgets/search_lineedit.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pymodaq_gui/utils/widgets/search_lineedit.py b/src/pymodaq_gui/utils/widgets/search_lineedit.py index 0a7d7a60..7cbcf1a6 100644 --- a/src/pymodaq_gui/utils/widgets/search_lineedit.py +++ b/src/pymodaq_gui/utils/widgets/search_lineedit.py @@ -13,7 +13,7 @@ def __init__(self, parent=None, debounce_ms=300): # Debounce timer self.debounce_ms = debounce_ms - self.search_timer = QTimer() + self.search_timer = QTimer(self) self.search_timer.setSingleShot(True) self.search_timer.timeout.connect(self._emit_debounced_search) @@ -48,6 +48,7 @@ def __init__(self, parent=None, debounce_ms=300): # Set fixed width for small widget self.setFixedWidth(200) + def _on_text_changed(self, text): """Called on every keystroke""" # Stop any pending search From 0afe774b70d12e62d1c5eec23b3b1804d5276613 Mon Sep 17 00:00:00 2001 From: Malik Irain Date: Thu, 30 Oct 2025 11:20:41 +0100 Subject: [PATCH 05/11] Add a Proxy to wrap a Widget and it's QAction to provide better PySide6 compatibility --- src/pymodaq_gui/managers/action_manager.py | 34 +++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/pymodaq_gui/managers/action_manager.py b/src/pymodaq_gui/managers/action_manager.py index 92035092..09d86062 100644 --- a/src/pymodaq_gui/managers/action_manager.py +++ b/src/pymodaq_gui/managers/action_manager.py @@ -163,10 +163,42 @@ def addwidget(klass: Union[str, QtWidgets.QWidget, object], *args, tip='', toolb return None if toolbar is not None: + class WidgetActionProxy(QtWidgets.QWidget): + ''' + Wrapper class of a Widget and its associated toolbar Action. + + All methods call are forwarded to the wrapped Widget. Even its class name + is copied. + + Only the setVisible method is different, as the Action need to be hidden. + + (monkey-patching setVisible on the widget wasn't compatible with PySide6) + ''' + def __init__(self, widget : QtWidgets.QWidget, action : QtWidgets.QAction): + super().__init__(widget.parent()) + self.setParent(widget) + + self._widget = widget + self._action = action + + def setVisible(self, visible : bool): + self._action.setVisible(visible) + self._widget.setVisible(visible) + super().setVisible(visible) + + def __getattr__(self, name : str): + print(f'calling {name}') + return getattr(self._widget, name) + + @property + def __class__(self): + return self._widget.__class__ + + action: QtWidgets.QAction = toolbar.addWidget(widget) action.setVisible(visible) action.setToolTip(tip) - widget.setVisible = action.setVisible #because visibility is only possible on the underlying QAction + widget = WidgetActionProxy(widget, action) else: widget.setVisible(visible) widget.setToolTip(tip) From 1bc247730b94a4e6882be5a7ae6366a084f8958a Mon Sep 17 00:00:00 2001 From: Malik Irain Date: Thu, 30 Oct 2025 11:33:31 +0100 Subject: [PATCH 06/11] Ensure tests don't delete objects twice by using deleteLater --- tests/managers/parameter_manager_test.py | 30 ++- .../parameter_manager_search_test.py | 184 ++++++++++++------ .../data_viewers_test/viewer0D_test.py | 9 +- .../viewer_interface_test.py | 74 +++++++ tests/utils/widgets/search_lineedit_test.py | 50 +++-- 5 files changed, 262 insertions(+), 85 deletions(-) create mode 100644 tests/plotting_test/data_viewers_test/viewer_interface_test.py diff --git a/tests/managers/parameter_manager_test.py b/tests/managers/parameter_manager_test.py index 79d6a5cf..4b05f7b2 100644 --- a/tests/managers/parameter_manager_test.py +++ b/tests/managers/parameter_manager_test.py @@ -1,3 +1,4 @@ + 1# -*- coding: utf-8 -*- """ Created the 07/11/2023 @@ -8,6 +9,7 @@ import pytest from qtpy import QtWidgets + from pyqtgraph.parametertree import Parameter from pymodaq_gui.examples.parameter_ex import ParameterEx @@ -18,6 +20,8 @@ from pymodaq_gui.managers.parameter_manager import ParameterManager + + @pytest.fixture def ini_qt_widget(init_qt): qtbot = init_qt @@ -41,29 +45,38 @@ class RealParameterManager(ParameterManager): ]}, -def test_parameter_manager(qtbot): +def test_parameter_manager_trace(qtbot): param_manager = RealParameterManager() - param_manager.settings_tree.show() + tree = param_manager.settings_tree + tree.show() + + # Assertions + assert hasattr(tree, 'header') + assert hasattr(tree, 'setMinimumHeight') + assert hasattr(tree, 'listAllItems') - assert hasattr(param_manager.settings_tree, 'header') - assert hasattr(param_manager.settings_tree, 'setMinimumHeight') - assert hasattr(param_manager.settings_tree, 'listAllItems') + # Optional: manually clean up to avoid qtbot deletion issues + tree.close() + tree.deleteLater() def test_save(qtbot, tmp_path): ptree = ParameterEx() - ptree.settings_tree.show() qtbot.addWidget(ptree.settings_tree) + ptree.settings_tree.show() file_path = tmp_path.joinpath('settings.xml') ptree.save_settings_slot(file_path) + ptree.settings_tree.close() + ptree.settings_tree.deleteLater() + def test_load(qtbot, tmp_path): ptree = ParameterEx() - ptree.settings_tree.show() qtbot.addWidget(ptree.settings_tree) + ptree.settings_tree.show() file_path = tmp_path.joinpath('settings.xml') ptree.save_settings_slot(file_path) @@ -98,3 +111,6 @@ def test_load(qtbot, tmp_path): assert compareValuesParameter(ptree.settings, parameter_copy) assert compareStructureParameter(ptree.settings, parameter_copy) + + ptree.settings_tree.close() + ptree.settings_tree.deleteLater() \ No newline at end of file diff --git a/tests/parameter_test/parameter_manager_search_test.py b/tests/parameter_test/parameter_manager_search_test.py index 3d8b5b47..dd7b6b25 100644 --- a/tests/parameter_test/parameter_manager_search_test.py +++ b/tests/parameter_test/parameter_manager_search_test.py @@ -7,10 +7,9 @@ from pymodaq_gui.managers.parameter_manager import ParameterManager, ParameterTreeWidget -@pytest.fixture -def sample_params(): - """Sample parameter structure for testing""" - return [ + +class RealParameterManager(ParameterManager): + params = [ { 'title': 'Main Settings', 'name': 'main_settings', @@ -18,7 +17,7 @@ def sample_params(): 'children': [ {'title': 'Detector Mode', 'name': 'detector_mode', 'type': 'list', 'limits': ['Single', 'Continuous'], 'value': 'Single'}, - {'title': 'Integration Time', 'name': 'integration_time', + {'title': 'Integration Time', 'name': 'integration_time', 'type': 'float', 'value': 100.0}, ] }, @@ -40,115 +39,164 @@ def sample_params(): } ] - -@pytest.fixture -def param_manager(qtbot, sample_params): - """Fixture providing a fresh ParameterManager instance for each test""" - manager = ParameterManager(settings_name='test_settings', + def __init__(self): + ParameterManager.__init__(self, settings_name='test_settings', action_list=('search', 'save', 'load')) - # Create fresh parameter tree for each test - manager.settings = Parameter.create(name='test_settings', type='group', - children=sample_params, showTop=False) - qtbot.addWidget(manager.settings_tree) - - yield manager - - # Cleanup after each test - manager.settings_tree.close() - manager.settings_tree.deleteLater() + + + class TestSearchBasics: """Core search functionality tests""" - - def test_search_widget_exists(self, param_manager:ParameterManager): + + def test_search_widget_exists(self, qtbot): """Test that search widget is created when 'search' in action_list""" + + param_manager = RealParameterManager() + qtbot.addWidget(param_manager.settings_tree) + param_manager.settings_tree.show() + qtbot.waitExposed(param_manager.settings_tree) search_widget = param_manager._settings_tree.get_action('search_settings') assert search_widget is not None assert isinstance(search_widget, QtWidgets.QLineEdit) - - def test_empty_search_shows_all(self, param_manager:ParameterManager): + + search_widget.close() + search_widget.deleteLater() + + param_manager.settings_tree.close() + param_manager.settings_tree.deleteLater() + + def test_empty_search_shows_all(self, qtbot): """Test that empty search shows all parameters""" + param_manager = RealParameterManager() + param_manager.settings_tree.show() + qtbot.waitExposed(param_manager.settings_tree) + qtbot.waitUntil(lambda: param_manager.settings_tree.isVisible()) + param_manager.search_settings_slot("") - + for item in param_manager.tree.listAllItems(): assert not item.isHidden() - - def test_search_filters_parameters(self, param_manager:ParameterManager): + + param_manager.settings_tree.close() + param_manager.settings_tree.deleteLater() + + def test_search_filters_parameters(self, qtbot): """Test that search hides non-matching parameters""" + param_manager = RealParameterManager() + qtbot.addWidget(param_manager.settings_tree) + param_manager.settings_tree.show() + qtbot.waitExposed(param_manager.settings_tree) + qtbot.waitUntil(lambda: param_manager.settings_tree.isVisible()) + param_manager.search_settings_slot("detector") - + # Check that some items are hidden all_items = param_manager.tree.listAllItems() hidden_items = [item for item in all_items if item.isHidden()] assert len(hidden_items) > 0 - - def test_search_case_insensitive(self, param_manager:ParameterManager): + + param_manager.settings_tree.close() + param_manager.settings_tree.deleteLater() + + def test_search_case_insensitive(self, qtbot): """Test that search is case-insensitive""" + param_manager = RealParameterManager() + qtbot.addWidget(param_manager.settings_tree) + param_manager.settings_tree.show() + qtbot.waitExposed(param_manager.settings_tree) + qtbot.waitUntil(lambda: param_manager.settings_tree.isVisible()) param_manager.search_settings_slot("DETECTOR") - - detector_items = [item for item in param_manager.tree.listAllItems() + + detector_items = [item for item in param_manager.tree.listAllItems() if 'detector' in item.param.title().lower()] - + for item in detector_items: assert not item.isHidden() - - def test_search_expands_parent_groups(self, param_manager:ParameterManager): + + param_manager.settings_tree.close() + param_manager.settings_tree.deleteLater() + + def test_search_expands_parent_groups(self, qtbot): """Test that parent groups expand when children match""" + param_manager = RealParameterManager() + qtbot.addWidget(param_manager.settings_tree) + param_manager.settings_tree.show() + qtbot.waitExposed(param_manager.settings_tree) + qtbot.waitUntil(lambda: param_manager.settings_tree.isVisible()) + param_manager.search_settings_slot("offset") - + # Find calibration parameter and check its expanded state calibration = param_manager.settings.child('advanced_settings', 'calibration') assert calibration is not None - + # Check if the parameter opts show it should be expanded assert calibration.opts.get('expanded', False) == True + param_manager.settings_tree.close() + param_manager.settings_tree.deleteLater() + class TestKeyboardShortcuts: """Keyboard shortcut tests""" - def test_ctrl_f_activates_search(self, qtbot, param_manager:ParameterManager): + def test_ctrl_f_activates_search(self, qtbot): """Test that Ctrl+F expands toolbar and focuses search""" - assert not param_manager._settings_tree.collapsible_widget.is_expanded - - # Show the widget and process events + param_manager = RealParameterManager() + qtbot.addWidget(param_manager.settings_tree) param_manager.settings_tree.show() - qtbot.waitForWindowShown(param_manager.settings_tree) - qtbot.wait(100) + # Show the widget and process events + qtbot.waitExposed(param_manager.settings_tree) + qtbot.waitUntil(lambda: param_manager.settings_tree.isVisible()) + + assert not param_manager._settings_tree.collapsible_widget.is_expanded # Trigger the shortcut directly param_manager._settings_tree.search_activate_shortcut.activated.emit() - qtbot.wait(200) # Give more time for animation and focus - + qtbot.waitUntil(lambda: param_manager._settings_tree.collapsible_widget.is_expanded, timeout=1000) + assert param_manager._settings_tree.collapsible_widget.is_expanded - + # Check that activate_search was called by verifying widget state search_widget = param_manager._settings_tree.get_action('search_settings') assert search_widget is not None - # Just verify the toolbar is expanded, focus is unreliable in tests - - def test_escape_collapses_toolbar(self, qtbot, param_manager:ParameterManager): + + param_manager.settings_tree.close() + param_manager.settings_tree.deleteLater() + + def test_escape_collapses_toolbar(self, qtbot): """Test that Escape key collapses toolbar""" + param_manager = RealParameterManager() + qtbot.addWidget(param_manager.settings_tree) param_manager.settings_tree.show() - qtbot.waitForWindowShown(param_manager.settings_tree) - + qtbot.waitExposed(param_manager.settings_tree) + qtbot.waitUntil(lambda: param_manager.settings_tree.isVisible()) + param_manager._settings_tree.activate_search() qtbot.wait(200) assert param_manager._settings_tree.collapsible_widget.is_expanded # Trigger the shortcut directly - param_manager._settings_tree.search_escape_shortcut.activated.emit() - qtbot.wait(200) + with qtbot.waitSignal(param_manager._settings_tree.search_escape_shortcut.activated, timeout=500) as blocker: + param_manager._settings_tree.search_escape_shortcut.activated.emit() assert not param_manager._settings_tree.collapsible_widget.is_expanded + param_manager.settings_tree.close() + param_manager.settings_tree.deleteLater() class TestSearchIntegration: """Integration tests""" - def test_toolbar_toggle_clears_filter(self, param_manager:ParameterManager): + def test_toolbar_toggle_clears_filter(self, qtbot): """Test that collapsing toolbar clears the filter""" + param_manager = RealParameterManager() + qtbot.addWidget(param_manager.settings_tree) + param_manager.settings_tree.show() + qtbot.waitExposed(param_manager.settings_tree) + qtbot.waitUntil(lambda: param_manager.settings_tree.isVisible()) param_manager.search_settings_slot("detector") assert param_manager._current_filter_text == "detector" @@ -156,9 +204,17 @@ def test_toolbar_toggle_clears_filter(self, param_manager:ParameterManager): param_manager.on_toolbar_toggled() assert param_manager._current_filter_text == "" - - def test_clear_search_restores_all(self, qtbot, param_manager:ParameterManager): + param_manager.settings_tree.close() + param_manager.settings_tree.deleteLater() + + def test_clear_search_restores_all(self, qtbot): """Test that clearing search restores all parameters""" + param_manager = RealParameterManager() + qtbot.addWidget(param_manager.settings_tree) + param_manager.settings_tree.show() + qtbot.waitExposed(param_manager.settings_tree) + qtbot.waitUntil(lambda: param_manager.settings_tree.isVisible()) + search_widget = param_manager._settings_tree.get_action('search_settings') qtbot.keyClicks(search_widget, "detector") @@ -169,14 +225,26 @@ def test_clear_search_restores_all(self, qtbot, param_manager:ParameterManager): for item in param_manager.tree.listAllItems(): assert not item.isHidden() - + + search_widget.close() + search_widget.deleteLater() + + param_manager.settings_tree.close() + param_manager.settings_tree.deleteLater() + + def test_initialization_without_search(self, qtbot): """Test that manager works without search in action_list""" + manager = ParameterManager(settings_name='test', action_list=('save', 'load')) qtbot.addWidget(manager.settings_tree) - + manager.settings_tree.show() + qtbot.waitExposed(manager.settings_tree) + qtbot.waitUntil(lambda: manager.settings_tree.isVisible()) assert not manager._settings_tree.get_action('search_settings').isVisible() + manager.settings_tree.close() + manager.settings_tree.deleteLater() if __name__ == '__main__': diff --git a/tests/plotting_test/data_viewers_test/viewer0D_test.py b/tests/plotting_test/data_viewers_test/viewer0D_test.py index 1568eca4..09cf4526 100644 --- a/tests/plotting_test/data_viewers_test/viewer0D_test.py +++ b/tests/plotting_test/data_viewers_test/viewer0D_test.py @@ -49,11 +49,13 @@ def test_init(self, init_viewer0d): prog = Viewer0D(None) assert isinstance(prog.parent, QtWidgets.QWidget) + prog.parent.deleteLater() def test_actions(self, init_viewer0d): prog, qtbot = init_viewer0d for action_name in ['clear', 'Nhistory', 'show_data_as_list']: assert prog.view.has_action(action_name) + prog.parent.deleteLater() def test_clear_action(self, init_viewer0d): prog, qtbot = init_viewer0d @@ -67,7 +69,7 @@ def test_clear_action(self, init_viewer0d): prog.view.get_action('clear').trigger() assert prog.view.data_displayer.axis.size == 0 - + prog.parent.deleteLater() def test_show_datalist(self, init_viewer0d): prog, qtbot = init_viewer0d @@ -77,7 +79,8 @@ def test_show_datalist(self, init_viewer0d): assert prog.view.values_list.isVisible() prog.view.get_action('show_data_as_list').trigger() assert not prog.view.values_list.isVisible() - + prog.parent.deleteLater() + def test_clear_data(self, init_viewer0d): prog, qtbot = init_viewer0d @@ -88,4 +91,4 @@ def test_clear_data(self, init_viewer0d): assert prog.view.data_displayer.axis.size != 0 prog.view.data_displayer.clear_data() assert prog.view.data_displayer.axis.size == 0 - + prog.parent.deleteLater() diff --git a/tests/plotting_test/data_viewers_test/viewer_interface_test.py b/tests/plotting_test/data_viewers_test/viewer_interface_test.py new file mode 100644 index 00000000..dfcdabe7 --- /dev/null +++ b/tests/plotting_test/data_viewers_test/viewer_interface_test.py @@ -0,0 +1,74 @@ +# import pytest +# import inspect +# from qtpy import QtWidgets +# from qtpy.QtCore import QObject +# +# from pymodaq_gui.plotting.data_viewers.viewer0D import Viewer0D +# from pymodaq_gui.plotting.data_viewers.viewer1D import Viewer1D +# from pymodaq_gui.plotting.data_viewers.viewer2D import Viewer2D +# +# @pytest.fixture +# def ini_qt_widget(qtbot): +# widget = QtWidgets.QWidget() +# qtbot.addWidget(widget) +# widget.show() +# yield qtbot, widget +# widget.close() +# +# +# @pytest.mark.parametrize("viewer_class, expected", [ +# (Viewer0D, { +# "show_data": True, +# "roi_manager": False, +# "ROI_changed": True, +# }), +# (Viewer1D, { +# "show_data": True, +# "roi_manager": True, +# "ROI_changed": True, +# }), +# (Viewer2D, { +# "show_data": True, +# "roi_manager": True, +# "ROI_changed": True, +# }), +# ]) +# def test_viewer_interface_consistency(ini_qt_widget, viewer_class, expected): +# qtbot, widget = ini_qt_widget +# viewer = viewer_class(widget) +# +# +# for attribute, should_exist in expected.items(): +# if should_exist: +# assert hasattr(viewer, attribute) +# if attribute == "sig_roi_changed": +# signal = getattr(viewer, attribute) +# assert isinstance(signal, QObject) +# else: +# assert not hasattr(viewer, attribute) +# +# +# +# def test_sig_roi_changed_argument_and_connection(ini_qt_widget): +# qtbot, widget = ini_qt_widget +# viewer = Viewer2D(widget) +# viewer.roi_manager.add_ROI(0, 'roi1') +# +# called_args = [] +# def test_slot(*args): +# called_args.append(args) +# +# viewer.roi_manager.sig_roi_changed.connect(test_slot) +# viewer.roi_manager.ROIs['roi1'].setPos((15, 15)) +# +# qtbot.waitUntil(lambda: len(called_args) > 0, timeout=1000) +# assert isinstance(called_args[0][0], str) +# +# +# #TODO with real signal/slot signature +# def test_slot_signature_matches_expected(): +# def on_roi_changed(self, roi_name: str): pass +# sig = inspect.signature(on_roi_changed) +# params = list(sig.parameters.values()) +# assert len(params) == 2 +# assert params[1].annotation in [str, inspect._empty] \ No newline at end of file diff --git a/tests/utils/widgets/search_lineedit_test.py b/tests/utils/widgets/search_lineedit_test.py index 3566d372..15a532b5 100644 --- a/tests/utils/widgets/search_lineedit_test.py +++ b/tests/utils/widgets/search_lineedit_test.py @@ -15,82 +15,98 @@ class TestSearchLineEdit: def test_empty_text_emits_immediately(self, qtbot, search_widget): """Clearing search should emit immediately without debounce""" + search_widget.setText("test") - + with qtbot.waitSignal(search_widget.searchTextChanged, timeout=200) as blocker: search_widget.clear() - + assert blocker.args[0] == "" + search_widget.deleteLater() def test_text_emits_after_debounce(self, qtbot, search_widget): """Non-empty text should emit after debounce delay""" with qtbot.waitSignal(search_widget.searchTextChanged, timeout=500) as blocker: search_widget.setText("test") - + assert blocker.args[0] == "test" + search_widget.deleteLater() + def test_rapid_changes_emit_once(self, qtbot, search_widget): """Rapid typing should result in single debounced signal""" signal_count = [] search_widget.searchTextChanged.connect(lambda t: signal_count.append(t)) - + search_widget.setText("t") search_widget.setText("te") search_widget.setText("tes") search_widget.setText("test") - - qtbot.wait(200) - + + with qtbot.waitSignal(search_widget.searchTextChanged, timeout=500) as _: + pass + assert len(signal_count) == 1 assert signal_count[0] == "test" + search_widget.deleteLater() def test_debounce_resets_on_new_input(self, qtbot, search_widget): """New input should reset debounce timer""" + + search_widget.show() + search_widget.setText("te") - qtbot.wait(50) # Less than debounce time + qtbot.wait(25) # Less than debounce time search_widget.setText("test") # Should reset timer - + with qtbot.waitSignal(search_widget.searchTextChanged, timeout=500) as blocker: pass - assert blocker.args[0] == "test" + search_widget.close() + search_widget.deleteLater() + def test_signal_not_emitted_before_debounce(self, qtbot, search_widget): """Signal should not emit before debounce time elapses""" signal_fired = [] search_widget.searchTextChanged.connect(lambda t: signal_fired.append(t)) - + search_widget.setText("test") qtbot.wait(50) # Less than 100ms debounce - + assert len(signal_fired) == 0 + search_widget.deleteLater() def test_custom_debounce_time(self, qtbot): """Custom debounce time should be respected""" widget = SearchLineEdit(debounce_ms=500) qtbot.addWidget(widget) - + assert widget.debounce_ms == 500 + widget.deleteLater() def test_search_icon_created(self, search_widget): """Search icon should be created and not null""" assert search_widget.search_icon is not None assert not search_widget.search_icon.isNull() + search_widget.deleteLater() def test_whitespace_only_emits_immediately(self, qtbot, search_widget): """Whitespace-only text should emit immediately like empty text""" with qtbot.waitSignal(search_widget.searchTextChanged, timeout=200) as blocker: search_widget.setText(" ") - + assert blocker.args[0] == " " + search_widget.deleteLater() def test_timer_stops_when_cleared(self, qtbot, search_widget): """Timer should stop when text is cleared""" search_widget.setText("test") assert search_widget.search_timer.isActive() - + search_widget.clear() qtbot.wait(50) - + # After clearing, timer should not be active (or restarted for immediate emit) - assert search_widget.text() == "" \ No newline at end of file + assert search_widget.text() == "" + search_widget.deleteLater() \ No newline at end of file From 7318d84410ee354b483c798a420b68faae08cce2 Mon Sep 17 00:00:00 2001 From: Malik Irain Date: Thu, 30 Oct 2025 11:36:02 +0100 Subject: [PATCH 07/11] removed debug print --- src/pymodaq_gui/managers/action_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pymodaq_gui/managers/action_manager.py b/src/pymodaq_gui/managers/action_manager.py index 09d86062..41587486 100644 --- a/src/pymodaq_gui/managers/action_manager.py +++ b/src/pymodaq_gui/managers/action_manager.py @@ -187,7 +187,6 @@ def setVisible(self, visible : bool): super().setVisible(visible) def __getattr__(self, name : str): - print(f'calling {name}') return getattr(self._widget, name) @property From 6045ede89d24b20bb46d837fe769b96c4205a528 Mon Sep 17 00:00:00 2001 From: Malik Irain Date: Thu, 30 Oct 2025 11:41:02 +0100 Subject: [PATCH 08/11] simplify workflow --- .github/workflows/tests.yml | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1265abde..cd818f0d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,10 +27,6 @@ jobs: python-version: ["3.9", "3.10", "3.11", "3.12"] qt-backend: ["pyqt5", "pyqt6", "pyside6"] runs-on: ${{ matrix.os }} - env: - DISPLAY: ':99' - QT_DEBUG_PLUGINS: 1 - steps: # Get the branch name for the badge generation - name: Extract branch name @@ -80,25 +76,7 @@ jobs: id: tests run: | mkdir coverage - if [[ "${{ runner.os }}" == "Linux" ]]; then - export QT_DEBUG_PLUGINS=1 - export QT_OPENGL=software - export QT_QPA_PLATFORM=offscreen - export LIBGL_ALWAYS_SOFTWARE=1 - export MESA_LOADER_DRIVER_OVERRIDE=softpipe - export QT_XCB_GL_INTEGRATION=none - - xvfb-run --server-args='-screen 0 1024x768x24' python -m pytest -vv --cov=pymodaq_gui - else - set QT_DEBUG_PLUGINS=1 - set QT_OPENGL=software - set QT_QPA_PLATFORM=offscreen - set LIBGL_ALWAYS_SOFTWARE=1 - set MESA_LOADER_DRIVER_OVERRIDE=softpipe - set QT_XCB_GL_INTEGRATION=none - - pytest -vv --cov=pymodaq_gui - fi + pytest -vv --cov=pymodaq_gui mv .coverage coverage/coverage_${{ matrix.os }}_${{ matrix.python-version }}_${{ matrix.qt-backend }} - name: Upload coverage artifact uses: actions/upload-artifact@v4.6.2 From c1b751a0ee48b0f8f288cabb63b96ad4ddfd2ef8 Mon Sep 17 00:00:00 2001 From: Malik Irain Date: Thu, 30 Oct 2025 12:04:20 +0100 Subject: [PATCH 09/11] Replaced processevents with qbot.wait --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cd818f0d..0b7b2c7b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -76,7 +76,7 @@ jobs: id: tests run: | mkdir coverage - pytest -vv --cov=pymodaq_gui + python -X faulthandler -m pytest --capture=no -vv --cov=pymodaq_gui mv .coverage coverage/coverage_${{ matrix.os }}_${{ matrix.python-version }}_${{ matrix.qt-backend }} - name: Upload coverage artifact uses: actions/upload-artifact@v4.6.2 From 329a78fa434a0020ab5f369236e372acc71d2aa6 Mon Sep 17 00:00:00 2001 From: Malik Irain Date: Thu, 30 Oct 2025 12:04:57 +0100 Subject: [PATCH 10/11] Debug command --- .../data_viewers_test/viewer2D_test.py | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/plotting_test/data_viewers_test/viewer2D_test.py b/tests/plotting_test/data_viewers_test/viewer2D_test.py index 63cc0e84..58325f64 100644 --- a/tests/plotting_test/data_viewers_test/viewer2D_test.py +++ b/tests/plotting_test/data_viewers_test/viewer2D_test.py @@ -73,27 +73,27 @@ def init_prog_show_data(init_viewer2D, distribution='uniform'): def create_one_roi(prog, qtbot, roitype='RectROI'): prog.view.get_action('roi').trigger() - QtWidgets.QApplication.processEvents() + qtbot.wait(0) with qtbot.waitSignal(prog.view.roi_manager.new_ROI_signal, timeout=10000) as blocker: prog.view.roi_manager.add_roi_programmatically(roitype) roi_name = blocker.args[0] roi = prog.view.roi_manager.get_roi(roi_name) index_roi = roi.index roi_type = roi.type() - QtWidgets.QApplication.processEvents() + qtbot.wait(0) return index_roi, roi, roi_type def copy_one_roi(prog, qtbot, roi ): prog.view.get_action('roi').trigger() - QtWidgets.QApplication.processEvents() + qtbot.wait(0) with qtbot.waitSignal(prog.view.roi_manager.new_ROI_signal, timeout=10000) as blocker: prog.view.roi_manager.copy_ROI(roi) roi_name = blocker.args[0] roi = prog.view.roi_manager.get_roi(roi_name) index_roi = roi.index roi_type = roi.type() - QtWidgets.QApplication.processEvents() + qtbot.wait(0) return index_roi, roi, roi_type @@ -230,7 +230,7 @@ def test_show_data_spread(self, init_viewer2D): def test_update_data_roi(self, init_prog_show_data): prog, qtbot, _ = init_prog_show_data create_one_roi(prog, qtbot) - QtWidgets.QApplication.processEvents() + qtbot.wait(0) with qtbot.waitSignal(prog.data_to_export_signal, timeout=1000) as blocker: prog.update_data() @@ -238,7 +238,7 @@ def test_update_data_roi(self, init_prog_show_data): def test_update_data_crosshair(self, init_prog_show_data): prog, qtbot, _ = init_prog_show_data prog.view.get_action('crosshair').trigger() - QtWidgets.QApplication.processEvents() + qtbot.wait(0) with qtbot.waitSignal(prog.crosshair_dragged, timeout=1000) as blocker: prog.update_data() @@ -398,7 +398,7 @@ def test_remove_roi(self, init_viewer2D): index_roi, roi, roi_type = create_one_roi(prog, qtbot, roitype='RectROI') prog.view.roi_manager.remove_roi_programmatically(index_roi) - QtWidgets.QApplication.processEvents() + qtbot.wait(0) def test_copy_roi(self, init_viewer2D): @@ -415,7 +415,7 @@ def test_copy_roi(self, init_viewer2D): prog.view.roi_manager.remove_roi_programmatically(index_roi) - QtWidgets.QApplication.processEvents() + qtbot.wait(0) def test_update_color_roi(self, init_viewer2D): prog, qtbot = init_viewer2D @@ -426,7 +426,7 @@ def test_update_color_roi(self, init_viewer2D): prog.view.roi_manager.settings.child('ROIs', roi_format(index_roi), 'Color').setValue('b') roi = prog.view.roi_manager.get_roi_from_index(index_roi) - QtWidgets.QApplication.processEvents() + qtbot.wait(0) assert roi.pen == mkPen('b') def test_data_from_roi(self, init_viewer2D): @@ -531,7 +531,7 @@ def test_change_isoline(self, init_viewer2D): prog.view.get_action('isocurve').trigger() prog.view.isocurver._isoLine.setValue(ISOLEVEL) prog.view.isocurver._isoLine.sigDragged.emit(prog.view.isocurver._isoLine) - QtWidgets.QApplication.processEvents() + qtbot.wait(0) assert prog.view.isocurver._isocurve_item.level == ISOLEVEL @@ -561,18 +561,18 @@ def test_crosshair_action(self, init_viewer2D): assert not prog.view.is_action_visible('position') prog.view.get_action('crosshair').trigger() - QtWidgets.QApplication.processEvents() + qtbot.wait(0) assert prog.view.is_action_checked('crosshair') assert prog.view.is_action_visible('position') assert prog.view.crosshair.isVisible() prog.view.get_action('crosshair').trigger() - QtWidgets.QApplication.processEvents() + qtbot.wait(0) prog.view.get_action('roi').trigger() # will keep lineout_widgets visible so we can check # if crosshair lineouts are still visible - QtWidgets.QApplication.processEvents() + qtbot.wait(0) assert not prog.view.is_action_checked('crosshair') assert not prog.view.is_action_visible('position') @@ -582,7 +582,7 @@ def test_setpos_crosshair(self, init_viewer2D): prog, qtbot = init_viewer2D data = init_data() prog.show_data(data) - QtWidgets.QApplication.processEvents() + qtbot.wait(0) XCROSS = 24 YCROSS = 75 @@ -627,7 +627,7 @@ def test_get_image(self, init_viewer2D): prog, qtbot = init_viewer2D data = init_data() prog.show_data(data) - QtWidgets.QApplication.processEvents() + qtbot.wait(0) with pytest.raises(KeyError): prog.view.data_displayer.get_image('not a valid image name') @@ -636,11 +636,11 @@ def test_update_display_items(self, init_viewer2D): prog, qtbot = init_viewer2D data = init_data() prog.show_data(data) - QtWidgets.QApplication.processEvents() + qtbot.wait(0) data = init_data(uniform=False) prog.show_data(data) - QtWidgets.QApplication.processEvents() + qtbot.wait(0) class TestModifyImages: @@ -653,7 +653,7 @@ def test_FlipUD_action(self, init_viewer2D): assert np.any(prog._datas[0] == approx(data[0])) prog.view.get_action('flip_ud').trigger() - QtWidgets.QApplication.processEvents() + qtbot.wait(0) assert np.any(prog._datas[0] == approx(np.flipud(data[0]))) def test_FlipLR_action(self, init_viewer2D): @@ -665,20 +665,20 @@ def test_FlipLR_action(self, init_viewer2D): prog.show_data(data) prog.view.get_action('flip_lr').trigger() - QtWidgets.QApplication.processEvents() + qtbot.wait(0) assert np.any(prog._datas[0] == approx(np.fliplr(data[0]))) prog.view.get_action('flip_lr').trigger() - QtWidgets.QApplication.processEvents() + qtbot.wait(0) assert np.any(prog._datas[0] == approx(data[0])) def test_rotate_action(self, init_viewer2D): prog, qtbot = init_viewer2D data = init_data() prog.show_data(data) - QtWidgets.QApplication.processEvents() + qtbot.wait(0) prog.view.get_action('rotate').trigger() - QtWidgets.QApplication.processEvents() + qtbot.wait(0) assert np.any(prog._datas[0] == approx(np.flipud(np.transpose(data[0])))) From 1499d2a4da5c1d2b7187b6f51854d7412e90c2fd Mon Sep 17 00:00:00 2001 From: Malik Irain Date: Thu, 30 Oct 2025 14:15:01 +0100 Subject: [PATCH 11/11] Set Viewer2D parent + some renaming --- .../data_viewers_test/viewer2D_test.py | 189 +++++++++--------- 1 file changed, 93 insertions(+), 96 deletions(-) diff --git a/tests/plotting_test/data_viewers_test/viewer2D_test.py b/tests/plotting_test/data_viewers_test/viewer2D_test.py index 58325f64..28292ea6 100644 --- a/tests/plotting_test/data_viewers_test/viewer2D_test.py +++ b/tests/plotting_test/data_viewers_test/viewer2D_test.py @@ -1,6 +1,5 @@ -from typing import Tuple - - +from typing import Tuple, Any, Generator +from pytestqt.qtbot import QtBot from qtpy import QtWidgets, QtCore from pymodaq_data import data as data_mod @@ -21,10 +20,6 @@ from pyqtgraph import mkPen -@fixture -def init_qt(qtbot): - return qtbot - def init_data(Ndata=1, uniform=True): Nx = 100 @@ -53,19 +48,21 @@ def init_data(Ndata=1, uniform=True): @fixture -def init_viewer2D(qtbot) -> Tuple[Viewer2D, None]: +def init_viewer2d(qtbot) -> Generator[tuple[Viewer2D, QtBot], None, None]: form = QtWidgets.QWidget() - prog = Viewer2D() + prog = Viewer2D(form) qtbot.addWidget(form) prog.parent.show() yield prog, qtbot + form.close() + form.deleteLater() @fixture -def init_prog_show_data(init_viewer2D, distribution='uniform'): - prog, qtbot = init_viewer2D +def init_prog_show_data(init_viewer2d, distribution='uniform'): + prog, qtbot = init_viewer2d data = init_data(3, uniform=(distribution == 'uniform')) prog.show_data(data) return prog, qtbot, data @@ -99,15 +96,15 @@ def copy_one_roi(prog, qtbot, roi ): class TestImageFactory: @pytest.mark.parametrize('item_type', ['uniform', 'spread']) - def test_create_image(self, init_qt, item_type): + def test_create_image(self, qtbot, item_type): image_item = v2d.image_item_factory(item_type=item_type) assert isinstance(image_item, pg.ImageItem) @pytest.mark.parametrize('axisOrder', ('row-major', 'col-major')) - def test_axisorder(self, init_qt, axisOrder): + def test_axisorder_valid(self, qtbot, axisOrder): image_item = v2d.image_item_factory('uniform', axisOrder) - def test_axisorder(self, init_qt): + def test_axisorder_invalid(self, qtbot): axisOrder = 'a random order' with pytest.raises(ValueError): image_item = v2d.image_item_factory('uniform', axisOrder) @@ -115,15 +112,15 @@ def test_axisorder(self, init_qt): class TestHistoFactory: @pytest.mark.parametrize('gradient', ['red', 'spread']) - def test_create_histo(self, init_qt, gradient): + def test_create_histo(self, qtbot, gradient): histo = v2d.histogram_factory(gradient=gradient) assert isinstance(histo, pg.HistogramLUTWidget) - def test_wrong_gradient(self, init_qt): + def test_wrong_gradient(self, qtbot): with pytest.raises(KeyError): histo = v2d.histogram_factory(gradient='yuipof135748f') - def test_set_image(self, init_qt): + def test_set_image(self, qtbot): image_item = pg.graphicsItems.ImageItem.ImageItem(np.random.rand(10,50)) histo = v2d.histogram_factory(image_item, gradient='red') @@ -131,7 +128,7 @@ def test_set_image(self, init_qt): class TestData0DWithHistory: - def test_add_datas_list(self, init_qt): + def test_add_datas_list(self, qtbot): Nsamplesinhisto = 2 data_histo = plot_utils.Data0DWithHistory(Nsamplesinhisto) dat = [[1, 2], [1, 2], [1, 2], [1, 2], [1, 2], [1, 2]] @@ -144,7 +141,7 @@ def test_add_datas_list(self, init_qt): assert 'data_00' in data_histo.datas assert 'data_01' in data_histo.datas - def test_add_datas(self, init_qt): + def test_add_datas(self, qtbot): data_histo = plot_utils.Data0DWithHistory() dat = [dict(CH0=1, CH1=2.), dict(CH0=np.array([1]), CH1=2.), dict(CH0=1, CH1=2.), dict(CH0=1, CH1=2.)] for ind, d in enumerate(dat): @@ -154,7 +151,7 @@ def test_add_datas(self, init_qt): assert 'CH0' in data_histo.datas assert 'CH1' in data_histo.datas - def test_add_datas_and_clear(self, init_qt): + def test_add_datas_and_clear(self, qtbot): data_histo = plot_utils.Data0DWithHistory() dat = [dict(CH0=1, CH1=2.), dict(CH0=np.array([1]), CH1=2.), dict(CH0=1, CH1=2.), dict(CH0=1, CH1=2.)] for ind, d in enumerate(dat): @@ -166,12 +163,12 @@ def test_add_datas_and_clear(self, init_qt): class TestViewer2D: - def test_init(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_init(self, init_viewer2d): + prog, qtbot = init_viewer2d assert isinstance(prog, Viewer2D) - def test_show_data_triggers_data_to_export_signal(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_show_data_triggers_data_to_export_signal(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() with qtbot.waitSignal(prog.data_to_export_signal, timeout=1000) as blocker: @@ -181,14 +178,14 @@ def test_show_data_triggers_data_to_export_signal(self, init_viewer2D): prog.show_data(data) @pytest.mark.xfail - def test_show_data_temp(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_show_data_temp(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() with qtbot.waitSignal(prog.data_to_export_signal, timeout=500) as blocker: prog.show_data_temp(data) - def test_show_data_setImageredblue(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_show_data_setImageredblue(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data(3) prog.show_data(data) @@ -199,8 +196,8 @@ def test_show_data_setImageredblue(self, init_viewer2D): assert prog.view.is_action_visible('green') assert prog.view.is_action_visible('blue') - def test_show_data_uniform(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_show_data_uniform(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data(2) with qtbot.waitSignal(prog.data_to_export_signal, timeout=1000) as blocker: prog.show_data(data) @@ -215,8 +212,8 @@ def test_show_data_uniform(self, init_viewer2D): assert prog.isdata['green'] assert not prog.isdata['blue'] - def test_show_data_spread(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_show_data_spread(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data(2, uniform=False) prog.show_data(data) @@ -246,22 +243,22 @@ def test_update_data_crosshair(self, init_prog_show_data): class TestAxis: @pytest.mark.parametrize('position', ('left', 'bottom', 'right', 'top')) - def test_axis_label(self, init_viewer2D, position): - prog, qtbot = init_viewer2D + def test_axis_label(self, init_viewer2d, position): + prog, qtbot = init_viewer2d UNITS= 'myunits' LABEL = 'mylabel' prog.view.set_axis_label(position, label=LABEL, units=UNITS) assert prog.view.get_axis_label(position) == (LABEL, UNITS) - def test_get_axis_error(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_get_axis_error(self, init_viewer2d): + prog, qtbot = init_viewer2d with pytest.raises(KeyError): prog.view.get_axis_label('unvalid key') - def test_scale_axis(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_scale_axis(self, init_viewer2d): + prog, qtbot = init_viewer2d XSCALING = 0.1 XOFFSET = 24 YSCALING = -2.1 @@ -286,13 +283,13 @@ class TestActions: @pytest.mark.parametrize('action', ['position', 'red', 'green', 'blue', 'autolevels', 'auto_levels_sym', 'histo', 'roi', 'isocurve', 'aspect_ratio', 'crosshair', 'ROIselect', 'flip_ud', 'flip_lr', 'rotate']) - def test_actionhas(self, init_viewer2D, action): - prog, qtbot = init_viewer2D + def test_actionhas(self, init_viewer2d, action): + prog, qtbot = init_viewer2d assert prog.view.has_action(action) @pytest.mark.parametrize('color', ['red', 'green', 'blue']) - def test_color_action(self, init_viewer2D, color): - prog, qtbot = init_viewer2D + def test_color_action(self, init_viewer2d, color): + prog, qtbot = init_viewer2d data = init_data(3) prog.show_data(data) @@ -304,8 +301,8 @@ def test_color_action(self, init_viewer2D, color): assert not prog.view.is_action_checked(color) assert not prog.view.data_displayer.get_image(color).isVisible() - def test_histo_action(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_histo_action(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data(2) prog.show_data(data) @@ -316,8 +313,8 @@ def test_histo_action(self, init_viewer2D): assert prog.view.histogrammer.get_histogram('green').isVisible() assert not prog.view.histogrammer.get_histogram('blue').isVisible() - def test_histo_autolevel_action(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_histo_autolevel_action(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data(3) prog.show_data(data) @@ -335,8 +332,8 @@ def test_histo_autolevel_action(self, init_viewer2D): assert prog.view.histogrammer.get_histogram('blue').getLevels() ==\ approx((5.693320370703248e-09, 9.83017824174412)) - def test_autolevel_action(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_autolevel_action(self, init_viewer2d): + prog, qtbot = init_viewer2d prog.view.get_action('autolevels').trigger() assert prog.view.histogrammer.autolevels assert prog.view.data_displayer.autolevels @@ -348,14 +345,14 @@ def test_autolevel_action(self, init_viewer2D): class TestHistogrammer: @pytest.mark.parametrize('color', ['red', 'green', 'blue']) - def test_get_histogram(self, init_viewer2D, color): - prog, qtbot = init_viewer2D + def test_get_histogram(self, init_viewer2d, color): + prog, qtbot = init_viewer2d assert color in prog.view.histogrammer.get_histograms() assert prog.view.histogrammer.get_histogram(color) == prog.view.histogrammer.get_histograms()[color] - def test_get_histogram_name_error(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_get_histogram_name_error(self, init_viewer2d): + prog, qtbot = init_viewer2d with pytest.raises(KeyError): prog.view.histogrammer.get_histogram('not a valid identifier') @@ -367,8 +364,8 @@ def test_setgradient(self, init_prog_show_data, histo, gradient): class TestROI: - def test_roi_action(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_roi_action(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() prog.show_data(data) create_one_roi(prog, qtbot, roitype='RectROI') @@ -376,8 +373,8 @@ def test_roi_action(self, init_viewer2D): assert prog.view.is_action_checked('roi') assert prog.view.roi_manager.roiwidget.isVisible() - def test_add_roi(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_add_roi(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() prog.show_data(data) @@ -390,8 +387,8 @@ def test_add_roi(self, init_viewer2D): assert isinstance(roi, EllipseROI) assert roi.index == 1 - def test_remove_roi(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_remove_roi(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() prog.show_data(data) @@ -401,8 +398,8 @@ def test_remove_roi(self, init_viewer2D): qtbot.wait(0) - def test_copy_roi(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_copy_roi(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() prog.show_data(data) @@ -417,8 +414,8 @@ def test_copy_roi(self, init_viewer2D): prog.view.roi_manager.remove_roi_programmatically(index_roi) qtbot.wait(0) - def test_update_color_roi(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_update_color_roi(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() prog.show_data(data) @@ -429,8 +426,8 @@ def test_update_color_roi(self, init_viewer2D): qtbot.wait(0) assert roi.pen == mkPen('b') - def test_data_from_roi(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_data_from_roi(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() prog.show_data(data) @@ -461,8 +458,8 @@ def test_data_from_roi(self, init_viewer2D): assert np.any(vlineout.data[0] == approx(np.mean(data[0], 1))) assert np.any(intlineout.data[0] == approx(np.mean(data[0]))) - def test_data_from_roi_spread(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_data_from_roi_spread(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data(uniform=False) prog.show_data(data) @@ -483,8 +480,8 @@ def test_data_from_roi_spread(self, init_viewer2D): assert f'Integrated_{roi_format(index_roi)}' in data_to_export.get_names('data0D') - def test_show_roi(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_show_roi(self, init_viewer2d): + prog, qtbot = init_viewer2d prog.show_roi(show=True, show_roi_widget=True) assert prog.is_action_checked('roi') assert prog.view.roi_manager.roiwidget.isVisible() @@ -504,8 +501,8 @@ def test_show_roi(self, init_viewer2D): class TestIsocurve: - def test_isocurve_action(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_isocurve_action(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() prog.show_data(data) @@ -516,13 +513,13 @@ def test_isocurve_action(self, init_viewer2D): @pytest.mark.parametrize('histo', ['blue', 'green', 'red']) @pytest.mark.parametrize('im_source', ['blue', 'green', 'red']) - def test_isocurve_parent(self, init_viewer2D, im_source, histo): - prog, qtbot = init_viewer2D + def test_isocurve_parent(self, init_viewer2d, im_source, histo): + prog, qtbot = init_viewer2d prog.view.isocurver.update_image_source(prog.view.data_displayer.get_image(im_source)) prog.view.isocurver.update_histogram_parent(prog.view.histogrammer.get_histogram(histo)) - def test_change_isoline(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_change_isoline(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() prog.show_data(data) @@ -536,8 +533,8 @@ def test_change_isoline(self, init_viewer2D): class TestAspectRatio: - def test_aspect_ratio_action(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_aspect_ratio_action(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() prog.show_data(data) @@ -554,8 +551,8 @@ def test_aspect_ratio_action(self, init_viewer2D): class TestCrosshair: - def test_crosshair_action(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_crosshair_action(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() prog.show_data(data) assert not prog.view.is_action_visible('position') @@ -578,8 +575,8 @@ def test_crosshair_action(self, init_viewer2D): assert not prog.view.is_action_visible('position') assert not prog.view.crosshair.isVisible() - def test_setpos_crosshair(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_setpos_crosshair(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() prog.show_data(data) qtbot.wait(0) @@ -595,8 +592,8 @@ def test_setpos_crosshair(self, init_viewer2D): assert blocker.args[0] == approx(XCROSS) assert blocker.args[1] == approx(YCROSS) - def test_crosshair_doubleclicked(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_crosshair_doubleclicked(self, init_viewer2d): + prog, qtbot = init_viewer2d prog.view.get_action('crosshair').trigger() with qtbot.waitSignal(prog.sig_double_clicked, timeout=10000) as blocker: prog.view.get_double_clicked().emit(10.5, 20.9) @@ -606,8 +603,8 @@ def test_crosshair_doubleclicked(self, init_viewer2D): class TestRoiSelect: - def test_ROIselect_action(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_ROIselect_action(self, init_viewer2d): + prog, qtbot = init_viewer2d SIZE = [20, 35] POS = [45, 123] prog.view.get_action('ROIselect').trigger() @@ -623,8 +620,8 @@ def test_ROIselect_action(self, init_viewer2D): class TestImageDisplayer: - def test_get_image(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_get_image(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() prog.show_data(data) qtbot.wait(0) @@ -632,8 +629,8 @@ def test_get_image(self, init_viewer2D): with pytest.raises(KeyError): prog.view.data_displayer.get_image('not a valid image name') - def test_update_display_items(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_update_display_items(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() prog.show_data(data) qtbot.wait(0) @@ -644,8 +641,8 @@ def test_update_display_items(self, init_viewer2D): class TestModifyImages: - def test_FlipUD_action(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_FlipUD_action(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() with qtbot.waitSignal(prog.data_to_export_signal, timeout=1000) as blocker: @@ -656,8 +653,8 @@ def test_FlipUD_action(self, init_viewer2D): qtbot.wait(0) assert np.any(prog._datas[0] == approx(np.flipud(data[0]))) - def test_FlipLR_action(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_FlipLR_action(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() prog.show_data(data) @@ -672,8 +669,8 @@ def test_FlipLR_action(self, init_viewer2D): qtbot.wait(0) assert np.any(prog._datas[0] == approx(data[0])) - def test_rotate_action(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_rotate_action(self, init_viewer2d): + prog, qtbot = init_viewer2d data = init_data() prog.show_data(data) qtbot.wait(0) @@ -683,8 +680,8 @@ def test_rotate_action(self, init_viewer2D): class TestMiscellanous: - def test_double_clicked(self, init_viewer2D): - prog, qtbot = init_viewer2D + def test_double_clicked(self, init_viewer2d): + prog, qtbot = init_viewer2d with qtbot.waitSignal(prog.sig_double_clicked, timeout=10000) as blocker: prog.view.get_double_clicked().emit(10.5, 20.9)