diff --git a/pyproject.toml b/pyproject.toml index 2c3464e..5d10a11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,8 @@ dependencies = [ "matplotlib", "jsonschema", "zhinst-toolkit >= 0.3.3", - "pyqt5", + "qtpy", + "pyside6", "versioningit ~= 2.2.0", ] @@ -59,7 +60,6 @@ build-file = "qumada/_version.py" addopts = [ "--import-mode=importlib", ] -qt_api = "pyqt5" [tool.coverage.run] source = [ diff --git a/src/legacy/mapping_gui_test.ipy b/src/legacy/mapping_gui_test.ipy index f7395fe..7fc38ef 100644 --- a/src/legacy/mapping_gui_test.ipy +++ b/src/legacy/mapping_gui_test.ipy @@ -84,7 +84,7 @@ script.setup( # %% -from PyQt5.QtWidgets import QApplication +from qtpy.QtWidgets import QApplication # %% print(QApplication.instance()) diff --git a/src/qumada/instrument/mapping/mapping_gui.py b/src/qumada/instrument/mapping/mapping_gui.py index 21a0741..127bf58 100644 --- a/src/qumada/instrument/mapping/mapping_gui.py +++ b/src/qumada/instrument/mapping/mapping_gui.py @@ -23,8 +23,14 @@ from collections.abc import Iterable, Mapping from typing import Any -from PyQt5.QtCore import QItemSelectionModel, Qt, QTimer, pyqtSignal, pyqtSlot -from PyQt5.QtGui import ( +from qcodes.instrument.channel import InstrumentModule +from qcodes.instrument.instrument import Instrument +from qcodes.instrument.parameter import Parameter +from qcodes.utils.metadata import Metadatable +from qtpy.QtCore import QItemSelectionModel, Qt, QTimer +from qtpy.QtCore import Signal as pyqtSignal +from qtpy.QtCore import Slot as pyqtSlot +from qtpy.QtGui import ( QBrush, QColor, QDropEvent, @@ -34,10 +40,9 @@ QStandardItem, QStandardItemModel, ) -from PyQt5.QtWidgets import ( +from qtpy.QtWidgets import ( QAction, QApplication, - QDesktopWidget, QHBoxLayout, QInputDialog, QLabel, @@ -51,10 +56,6 @@ QVBoxLayout, QWidget, ) -from qcodes.instrument.channel import InstrumentModule -from qcodes.instrument.instrument import Instrument -from qcodes.instrument.parameter import Parameter -from qcodes.utils.metadata import Metadatable from qumada.instrument.mapping.base import TerminalParameters, filter_flatten_parameters from qumada.metadata import Metadata @@ -203,7 +204,8 @@ def update_tree(self): def import_data(self, terminal_parameters: TerminalParameters) -> None: """Build up tree with provided terminal parameters.""" - root = self.model().invisibleRootItem() + model = self.model() + root = model.invisibleRootItem() self.terminal_parameters = terminal_parameters for terminal_name, terminal_params in terminal_parameters.items(): item = QStandardItem(terminal_name) @@ -221,13 +223,18 @@ def import_data(self, terminal_parameters: TerminalParameters) -> None: item.appendRow(subitem) qidx = item.index() - self.model().setData(qidx.siblingAtColumn(1), QBrush(RED), Qt.BackgroundRole) - self.model().insertColumn(1, qidx) - self.model().insertColumn(2, qidx) + model.setData(qidx.siblingAtColumn(1), QBrush(RED), Qt.BackgroundRole) + model.insertColumn(1, qidx) + model.insertColumn(2, qidx) + for i in range(len(terminal_params.keys())): - self.model().setData(qidx.child(i, 1), "") - self.model().setData(qidx.child(i, 1), QBrush(RED), Qt.BackgroundRole) - self.model().setData(qidx.child(i, 2), "") + # this is written with questionable LLM support + idx1 = model.index(i, 1, qidx) + idx2 = model.index(i, 2, qidx) + + self.model().setData(idx1, "") + self.model().setData(idx1, QBrush(RED), Qt.BackgroundRole) + self.model().setData(idx2, "") self.setColumnHidden(2, not self.monitoring_enable) @@ -590,7 +597,10 @@ def __init__( idx = self.terminal_tree.model().invisibleRootItem().child(0, 0).index() self.terminal_tree.selectionModel().select(idx, QItemSelectionModel.SelectionFlag.ClearAndSelect) self.terminal_tree.setCurrentIndex(idx) - self.resize(QDesktopWidget().availableGeometry(self).size() * 0.45) + + screen = self.screen() or QApplication.primaryScreen() + new_size = screen.availableGeometry().size() * 0.45 + self.resize(new_size) self.terminal_parameters = terminal_parameters diff --git a/src/qumada/utils/device_GUI.py b/src/qumada/utils/device_GUI.py index 3df8e95..973b3af 100644 --- a/src/qumada/utils/device_GUI.py +++ b/src/qumada/utils/device_GUI.py @@ -3,17 +3,17 @@ import sys import threading -from PyQt5.QtCore import ( +from qtpy.QtCore import ( Q_ARG, QMetaObject, QObject, Qt, QThread, QTimer, - pyqtSignal, - pyqtSlot, ) -from PyQt5.QtWidgets import ( +from qtpy.QtCore import Signal as pyqtSignal +from qtpy.QtCore import Slot as pyqtSlot +from qtpy.QtWidgets import ( QApplication, QLabel, QPushButton, diff --git a/src/tests/device_gui_test.py b/src/tests/device_gui_test.py new file mode 100644 index 0000000..7425cfb --- /dev/null +++ b/src/tests/device_gui_test.py @@ -0,0 +1,88 @@ +""" +This test was generated by an LLM. + +It does not test actual functionality. It just tests that the module device_GUI.py can be imported and that some code +paths don't raise an exception. +""" + +import multiprocessing as mp + +import pytest +from qtpy.QtCore import QObject +from qtpy.QtCore import Signal as pyqtSignal + +import qumada.utils.device_GUI as device_GUI + + +class DummyWorker(QObject): + """Deterministic replacement for device_GUI.Worker used in most tests.""" + + data_ready = pyqtSignal(list) + + def __init__(self, interval, parameters): + super().__init__() + self.interval = interval + self.parameters = parameters + self.running = True + + def stop(self): + self.running = False + + +@pytest.fixture +def parameters(): + p1 = device_GUI.Parent("P1") + p2 = device_GUI.Parent("P2") + return [ + device_GUI.Parameter("A", p1), + device_GUI.Parameter("B", p1), + device_GUI.Parameter("C", p2), + ] + + +@pytest.fixture +def gui(qtbot, parameters, monkeypatch): + # Replace Worker with a deterministic stub + monkeypatch.setattr(device_GUI, "Worker", DummyWorker) + q = mp.Queue() + w = device_GUI.MeasurementGUI(parameters, q) + qtbot.addWidget(w) + # keep intervals small if anything uses it + w.interval_spinbox.setValue(120) + return w + + +def _col_texts(table, col): + return [(table.item(r, col).text() if table.item(r, col) is not None else "") for r in range(table.rowCount())] + + +def test_initial_table_population(gui, parameters): + assert gui.table.rowCount() == len(parameters) + # Column 0: names "Param: " + names = _col_texts(gui.table, 0) + assert names == [f"Param: {p._parent.name} {p.name}" for p in parameters] + # Column 1: initial values from __call__() -> "42" + values = _col_texts(gui.table, 1) + assert values == ["42"] * len(parameters) + + +def test_update_interval_changes_worker_field(gui, qtbot): + # Initial value is 120 from fixture + assert gui.worker.interval == 120 + gui.interval_spinbox.setValue(333) + # Signal is connected in initUI; the slot sets worker.interval + assert gui.worker.interval == 333 + + +def test_close_event_stops_worker_and_puts_quit(gui, qtbot): + q = gui.data_queue + # Thread should be running before close + assert gui.worker_thread.isRunning() + gui.close() + + # After close, thread should be stopped + assert not gui.worker_thread.isRunning() + # Worker stop() should have been called + assert gui.worker.running is False + # Queue should receive "QUIT" + assert q.get(timeout=1) == "QUIT" diff --git a/src/tests/mapping_test.py b/src/tests/mapping_test.py index 2db714d..8340f8e 100644 --- a/src/tests/mapping_test.py +++ b/src/tests/mapping_test.py @@ -28,8 +28,6 @@ import pytest from jsonschema import ValidationError -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QApplication, QMessageBox from pytest_cases import fixture_ref, parametrize from pytest_mock import MockerFixture from qcodes.instrument_drivers.mock_instruments import ( @@ -37,6 +35,8 @@ DummyInstrument, ) from qcodes.station import Station +from qtpy.QtCore import Qt +from qtpy.QtWidgets import QApplication, QMessageBox import qumada.instrument.mapping as mapping from qumada.instrument.custom_drivers.Dummies.dummy_dac import DummyDac