Skip to content
This repository was archived by the owner on Nov 6, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -57,7 +53,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
Expand All @@ -66,19 +66,17 @@ 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
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
Expand Down
33 changes: 32 additions & 1 deletion src/pymodaq_gui/managers/action_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,41 @@ 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):
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)
Expand Down
16 changes: 11 additions & 5 deletions src/pymodaq_gui/plotting/items/roi.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,20 +100,23 @@ def get_descriptors_from_dimensionality(cls, dim: DataDim):
return descriptors


class ROIMixin(QtCore.QObject):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's not inheriting from QObject one cannot create and use signal. I guess because it's a mixin, it's no big deal if used within another class inheriting from QObject? Or maybe that was the issue?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it's a Mixin class it's not supposed to be used by itself and it's actually used by another class inheriting QObject. So in the end, QObject was initialized twice. When done in normal execution it's fine (or at least it doesn't crash) but in pytest it seems to be the source of segfaults.

I tried several ways to solve the problem but this was the only functional one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok understood! THere are still tests not working ;-)

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)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -343,7 +350,6 @@ def setPen(self, color):
def color(self):
return self.brush.color()


@ROIFactory.register()
class EllipseROI(ROI):
"""
Expand Down
3 changes: 2 additions & 1 deletion src/pymodaq_gui/utils/widgets/search_lineedit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
30 changes: 23 additions & 7 deletions tests/managers/parameter_manager_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

1# -*- coding: utf-8 -*-
"""
Created the 07/11/2023
Expand All @@ -8,6 +9,7 @@
import pytest
from qtpy import QtWidgets


from pyqtgraph.parametertree import Parameter

from pymodaq_gui.examples.parameter_ex import ParameterEx
Expand All @@ -18,6 +20,8 @@
from pymodaq_gui.managers.parameter_manager import ParameterManager




@pytest.fixture
def ini_qt_widget(init_qt):
qtbot = init_qt
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Loading