From 19be96300e7754ef45a13d8c90a4119b331a74d4 Mon Sep 17 00:00:00 2001 From: Edoardo Pasca <14138589+paskino@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:22:19 +0000 Subject: [PATCH] Use QtPy abstraction layer (#144). GHA tests with PySide2 and PyQt5 (#146) - Use `qtpy` as virtual Qt binding package. GHA unit tests are run with PySide2 and PyQt5 (#146) - Add `pyqt_env.yml` and `pyside_env.yml` environment files (#146) - Update `CONTRIBUTING.md`, `README.md` and add documentation file (#146) Co-authored-by: Sam Tygier Danica Sugic --- .github/workflows/test.yml | 3 + CHANGELOG.md | 5 ++ CONTRIBUTING.md | 9 ++ Documentation.md | 78 +++++++++++++++++ README.md | 87 ++----------------- eqt/threading/QtThreading.py | 4 +- eqt/ui/FormDialog.py | 2 +- eqt/ui/MainWindowWithProgressDialogs.py | 6 +- eqt/ui/MainWindowWithSessionManagement.py | 4 +- eqt/ui/NoBorderScrollArea.py | 2 +- eqt/ui/ProgressTimerDialog.py | 6 +- eqt/ui/ReOrderableListWidget.py | 2 +- eqt/ui/SessionDialogs.py | 4 +- eqt/ui/UIFormWidget.py | 2 +- eqt/ui/UIMultiStepWidget.py | 6 +- eqt/ui/UISliderWidget.py | 4 +- eqt/ui/UIStackedWidget.py | 6 +- ...MainWindowWithSessionManagement_example.py | 4 +- examples/NoBorderScrollArea_example.py | 4 +- examples/advanced_dialog_example.py | 2 +- examples/dialog_example.py | 2 +- examples/dialog_example_2.py | 2 +- examples/dialog_example_3_save_default.py | 2 +- examples/dialog_multistep_example.py | 2 +- examples/dialog_save_state_example.py | 4 +- examples/insert_widgets_example.py | 2 +- examples/progress_timer_dialog_example.py | 4 +- examples/remove_widgets_example.py | 2 +- examples/reorderable_list_widget_example.py | 2 +- examples/utilitiesForExamples.py | 2 +- pyproject.toml | 2 +- recipe/eqt_env.yml | 2 +- recipe/pyqt_env.yml | 7 ++ recipe/pyside_env.yml | 5 ++ test/__init__.py | 2 +- test/dialog_example_2_test.py | 6 +- test/test_MainWindowWithSessionManagement.py | 15 ++-- test/test_NoBorderScrollArea.py | 2 +- test/test_SessionDialogs.py | 12 ++- test/test__formUI_status_test.py | 6 +- 40 files changed, 179 insertions(+), 144 deletions(-) create mode 100644 Documentation.md create mode 100644 recipe/pyqt_env.yml create mode 100644 recipe/pyside_env.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c603ec8..2acc3d9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,7 @@ jobs: strategy: matrix: python: [3.8, 3.11] + qtbindings: ['PySide2', 'PyQt5'] steps: - uses: actions/checkout@v4 with: @@ -17,7 +18,9 @@ jobs: - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} + qt-bindings: ${{ matrix.qtbindings }} - run: pip install -U .[dev] + - run: pip install ${{ matrix.qtbindings }} - run: pytest deploy: needs: [test] diff --git a/CHANGELOG.md b/CHANGELOG.md index 05a4ad6..6d6d61f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# Version 2.0.0 +- Use `qtpy` as virtual Qt binding package. GHA unit tests are run with PySide2 and PyQt5 (#146) +- Add `pyqt_env.yml` and `pyside_env.yml` environment files (#146) +- Update `CONTRIBUTING.md`, `README.md` and add documentation file (#146) + # Version 1.0.2 - Upgrade python to 3.8 in `test.yml` (#171) - Rename `/scripts` directory to `/recipe` (#161) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6182389..d7ead0b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,6 +32,15 @@ cd eqt mamba env create -f recipe/eqt_env.yml ``` +`eqt` uses the [`qtpy`](https://github.com/spyder-ide/qtpy) abstraction layer for Qt bindings, meaning that it works with either PySide or PyQt bindings. Thus, `eqt_env` does not depend on either. The environment can be updated with either `pyside2` or `pyqt5`, as follows. +```sh +mamba env update --name eqt_env --file pyside_env.yml +``` +or +```sh +mamba env update --name eqt_env --file pyqt_env.yml +``` + 4. Activate the environment: ```sh mamba activate eqt_env diff --git a/Documentation.md b/Documentation.md new file mode 100644 index 0000000..ab3f8f3 --- /dev/null +++ b/Documentation.md @@ -0,0 +1,78 @@ +# Documentation for eqt + +## Running asynchronous tasks + +To run a function in a separate thread we use a `Worker` which is a subclass of a `QRunnable`. + +For the `Worker` to work one needs to define: + +1. the function that does what you need +2. Optional callback methods to get the status of the thread by means of `QtCore.QSignal`s + +On [initialisation](https://github.com/TomographicImaging/eqt/blob/535e487d09d928713d7d6aa1123657597627c4b0/eqt/threading/QtThreading.py#L32-L38) of the `Worker` the user needs to pass the function that has to run in the thread, i.e. `fn` below, and additional optional positional and keyword arguments, which will be passed on to the actual function that is run in the `QRunnable`. + +```python +class Worker(QtCore.QRunnable): + def __init__(self, fn, *args, **kwargs): + self.fn = fn + self.args = args + self.kwargs = kwargs + self.signals = WorkerSignals() +``` + +In practice the user will need to pass to the `Worker` as many parameters as there are listed in the [function](https://github.com/TomographicImaging/eqt/blob/535e487d09d928713d7d6aa1123657597627c4b0/eqt/threading/QtThreading.py#L56) to be run. + +```python +result = self.fn(*self.args, **self.kwargs) +``` + +But `Worker` will [add](https://github.com/TomographicImaging/eqt/blob/535e487d09d928713d7d6aa1123657597627c4b0/eqt/threading/QtThreading.py#L41-L43) to the `**kwargs` the following `QSignal`. + +```python + # Add progress callback to kwargs + self.kwargs['progress_callback'] = self.signals.progress + self.kwargs['message_callback'] = self.signals.message + self.kwargs['status_callback'] = self.signals.status +``` + +Therefore it is advisable to always have `**kwargs` in the function `fn` signature so that you can access the `QSignal` and emit the signal required. For instance one could emit a progress by: + +```python +def fn(num_iter, **kwargs): + progress_callback = kwargs.get('progress_callback', None) + for i in range(num_iter): + do_something + if progress_callback is not None: + progress_callback.emit( i ) +``` + +### Passing a signal to a Worker + +This is done just after one has defined the `Worker`: + +```python +def handle_progress(num_iter): + # do something with the progress + print ("Current progress is ", num_iter) + +worker = Worker(fn, 10) +worker.signals.progress.connect(handle_progress) +``` + +So, each time `fn` comes to `progress_callback.emit( i )` the function `handle_progress` will be called with the parameter `i` of its `for` loop. + +### Signals available + +The signals that are available in the `Worker` class are defined in [`WorkerSignal`](https://github.com/TomographicImaging/eqt/blob/535e487d09d928713d7d6aa1123657597627c4b0/eqt/threading/QtThreading.py#L66) and are the following. Below you can also see the type of data that each signal can emit. + +```python +finished = QtCore.Signal() +error = QtCore.Signal(tuple) +result = QtCore.Signal(object) + +progress = QtCore.Signal(int) +message = QtCore.Signal(str) +status = QtCore.Signal(tuple) +``` + +Read more on [Qt signals and slots](https://doc.qt.io/qt-5/signalsandslots.html) and on how to use them in [PySide2](https://wiki.qt.io/Qt_for_Python_Signals_and_Slots). diff --git a/README.md b/README.md index 8d2325c..0d4ef57 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,13 @@ Templates & tools to develop Qt GUIs in Python. -One use case is accepting user input while running another task asynchronously (so that the UI is still responsive). - +Some example classes are 1. `UIFormWidget`: a class to help creating Qt forms programmatically, useable in `QDockWidgets` and `QWidget` 2. `FormDialog`: a `QDialog` with a form inside with OK and Cancel buttons 3. `Worker`: a class that defines a `QRunnable` to handle worker thread setup, signals and wrap up +One use case is accepting a user input while running another task asynchronously (so that the UI is still responsive). + ## Installation Via `pip`/`conda`/`mamba`, i.e. any of the following: @@ -18,86 +19,16 @@ Via `pip`/`conda`/`mamba`, i.e. any of the following: - `conda install -c conda-forge eqt` - `mamba install -c conda-forge eqt` -## Examples - -See the [`examples`](examples) directory, e.g. how to launch a `QDialog` with a form inside using `eqt`'s [`QWidget`](examples/dialog_example.py) or [`FormDialog`](examples/dialog_example_2.py). - -### Running asynchronous tasks - -To run a function in a separate thread we use a `Worker` which is a subclass of a `QRunnable`. - -For the `Worker` to work one needs to define: - -1. the function that does what you need -2. Optional callback methods to get the status of the thread by means of `QtCore.QSignal`s - -On [initialisation](https://github.com/TomographicImaging/eqt/blob/535e487d09d928713d7d6aa1123657597627c4b0/eqt/threading/QtThreading.py#L32-L38) of the `Worker` the user needs to pass the function that has to run in the thread, i.e. `fn` below, and additional optional positional and keyword arguments, which will be passed on to the actual function that is run in the `QRunnable`. - -```python -class Worker(QtCore.QRunnable): - def __init__(self, fn, *args, **kwargs): - self.fn = fn - self.args = args - self.kwargs = kwargs - self.signals = WorkerSignals() -``` - -In practice the user will need to pass to the `Worker` as many parameters as there are listed in the [function](https://github.com/TomographicImaging/eqt/blob/535e487d09d928713d7d6aa1123657597627c4b0/eqt/threading/QtThreading.py#L56) to be run. - -```python -result = self.fn(*self.args, **self.kwargs) -``` -But `Worker` will [add](https://github.com/TomographicImaging/eqt/blob/535e487d09d928713d7d6aa1123657597627c4b0/eqt/threading/QtThreading.py#L41-L43) to the `**kwargs` the following `QSignal`. +#### Note: +`eqt` uses the [`qtpy`](https://github.com/spyder-ide/qtpy) abstraction layer for Qt bindings, meaning that it works with either PySide or PyQt bindings. Thus, the package does not depend on either. If the environment does not already have a Qt binding then the user *must* install either `pyside2` or `pyqt5`. -```python - # Add progress callback to kwargs - self.kwargs['progress_callback'] = self.signals.progress - self.kwargs['message_callback'] = self.signals.message - self.kwargs['status_callback'] = self.signals.status -``` - -Therefore it is advisable to always have `**kwargs` in the function `fn` signature so that you can access the `QSignal` and emit the signal required. For instance one could emit a progress by: - -```python -def fn(num_iter, **kwargs): - progress_callback = kwargs.get('progress_callback', None) - for i in range(num_iter): - do_something - if progress_callback is not None: - progress_callback.emit( i ) -``` - -### Passing a signal to a Worker - -This is done just after one has defined the `Worker`: - -```python -def handle_progress(num_iter): - # do something with the progress - print ("Current progress is ", num_iter) - -worker = Worker(fn, 10) -worker.signals.progress.connect(handle_progress) -``` - -So, each time `fn` comes to `progress_callback.emit( i )` the function `handle_progress` will be called with the parameter `i` of its `for` loop. - -### Signals available - -The signals that are available in the `Worker` class are defined in [`WorkerSignal`](https://github.com/TomographicImaging/eqt/blob/535e487d09d928713d7d6aa1123657597627c4b0/eqt/threading/QtThreading.py#L66) and are the following. Below you can also see the type of data that each signal can emit. - -```python -finished = QtCore.Signal() -error = QtCore.Signal(tuple) -result = QtCore.Signal(object) +## Examples -progress = QtCore.Signal(int) -message = QtCore.Signal(str) -status = QtCore.Signal(tuple) -``` +See the [`examples`](examples) directory, e.g. how to launch a `QDialog` with a form inside using `eqt`'s [`QWidget`](examples/dialog_example.py) or [`FormDialog`](examples/dialog_example_2.py). -Read more on [Qt signals and slots](https://doc.qt.io/qt-5/signalsandslots.html) and on how to use them in [PySide2](https://wiki.qt.io/Qt_for_Python_Signals_and_Slots). +## Documentation +See [Documentation.md](./Documentation.md). ## Developer Contribution Guide See [CONTRIBUTING.md](./CONTRIBUTING.md). diff --git a/eqt/threading/QtThreading.py b/eqt/threading/QtThreading.py index faec586..7804228 100644 --- a/eqt/threading/QtThreading.py +++ b/eqt/threading/QtThreading.py @@ -7,8 +7,8 @@ # https://www.geeksforgeeks.org/migrate-pyqt5-app-to-pyside2 import traceback -from PySide2 import QtCore -from PySide2.QtCore import Slot +from qtpy import QtCore +from qtpy.QtCore import Slot class Worker(QtCore.QRunnable): diff --git a/eqt/ui/FormDialog.py b/eqt/ui/FormDialog.py index f9f8e9b..8324a2f 100644 --- a/eqt/ui/FormDialog.py +++ b/eqt/ui/FormDialog.py @@ -1,4 +1,4 @@ -from PySide2 import QtCore, QtWidgets +from qtpy import QtCore, QtWidgets from . import UIFormFactory diff --git a/eqt/ui/MainWindowWithProgressDialogs.py b/eqt/ui/MainWindowWithProgressDialogs.py index 0bc3575..1893eaa 100644 --- a/eqt/ui/MainWindowWithProgressDialogs.py +++ b/eqt/ui/MainWindowWithProgressDialogs.py @@ -1,9 +1,9 @@ import qdarkstyle -from PySide2.QtCore import QSettings, QThreadPool -from PySide2.QtGui import QKeySequence -from PySide2.QtWidgets import QAction, QMainWindow from qdarkstyle.dark.palette import DarkPalette from qdarkstyle.light.palette import LightPalette +from qtpy.QtCore import QSettings, QThreadPool +from qtpy.QtGui import QKeySequence +from qtpy.QtWidgets import QAction, QMainWindow from .ProgressTimerDialog import ProgressTimerDialog from .SessionDialogs import AppSettingsDialog diff --git a/eqt/ui/MainWindowWithSessionManagement.py b/eqt/ui/MainWindowWithSessionManagement.py index c28e3cf..f42bc75 100644 --- a/eqt/ui/MainWindowWithSessionManagement.py +++ b/eqt/ui/MainWindowWithSessionManagement.py @@ -4,8 +4,8 @@ from datetime import datetime from functools import partial -from PySide2.QtGui import QCloseEvent, QKeySequence -from PySide2.QtWidgets import QAction +from qtpy.QtGui import QCloseEvent, QKeySequence +from qtpy.QtWidgets import QAction from ..io import zip_directory from ..threading import Worker diff --git a/eqt/ui/NoBorderScrollArea.py b/eqt/ui/NoBorderScrollArea.py index 2fd40d1..fdb2442 100644 --- a/eqt/ui/NoBorderScrollArea.py +++ b/eqt/ui/NoBorderScrollArea.py @@ -1,7 +1,7 @@ import re import qdarkstyle -from PySide2.QtWidgets import QPushButton, QScrollArea, QWidget +from qtpy.QtWidgets import QPushButton, QScrollArea, QWidget class NoBorderScrollArea(QScrollArea): diff --git a/eqt/ui/ProgressTimerDialog.py b/eqt/ui/ProgressTimerDialog.py index edf370a..0968f4a 100644 --- a/eqt/ui/ProgressTimerDialog.py +++ b/eqt/ui/ProgressTimerDialog.py @@ -1,9 +1,9 @@ import time from time import sleep -from PySide2 import QtCore -from PySide2.QtCore import Qt, QThreadPool -from PySide2.QtWidgets import QProgressDialog +from qtpy import QtCore +from qtpy.QtCore import Qt, QThreadPool +from qtpy.QtWidgets import QProgressDialog from ..threading import Worker diff --git a/eqt/ui/ReOrderableListWidget.py b/eqt/ui/ReOrderableListWidget.py index 68de6da..6bd3fdc 100644 --- a/eqt/ui/ReOrderableListWidget.py +++ b/eqt/ui/ReOrderableListWidget.py @@ -1,4 +1,4 @@ -from PySide2 import QtCore, QtWidgets +from qtpy import QtCore, QtWidgets class ReOrderableListWidget(QtWidgets.QTableWidget): diff --git a/eqt/ui/SessionDialogs.py b/eqt/ui/SessionDialogs.py index cf5a35d..b8b0032 100644 --- a/eqt/ui/SessionDialogs.py +++ b/eqt/ui/SessionDialogs.py @@ -1,7 +1,7 @@ import os -from PySide2 import QtWidgets -from PySide2.QtWidgets import ( +from qtpy import QtWidgets +from qtpy.QtWidgets import ( QCheckBox, QComboBox, QFileDialog, diff --git a/eqt/ui/UIFormWidget.py b/eqt/ui/UIFormWidget.py index 3e067db..0a085e7 100644 --- a/eqt/ui/UIFormWidget.py +++ b/eqt/ui/UIFormWidget.py @@ -1,6 +1,6 @@ from warnings import warn -from PySide2 import QtWidgets +from qtpy import QtWidgets from .UISliderWidget import UISliderWidget diff --git a/eqt/ui/UIMultiStepWidget.py b/eqt/ui/UIMultiStepWidget.py index 63f9b82..8d877df 100644 --- a/eqt/ui/UIMultiStepWidget.py +++ b/eqt/ui/UIMultiStepWidget.py @@ -1,6 +1,6 @@ -from PySide2 import QtWidgets -from PySide2.QtCore import Qt -from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QPushButton +from qtpy import QtWidgets +from qtpy.QtCore import Qt +from qtpy.QtWidgets import QGroupBox, QHBoxLayout, QPushButton class UIMultiStepWidget(object): diff --git a/eqt/ui/UISliderWidget.py b/eqt/ui/UISliderWidget.py index c5722cb..86f39dc 100644 --- a/eqt/ui/UISliderWidget.py +++ b/eqt/ui/UISliderWidget.py @@ -1,5 +1,5 @@ -from PySide2 import QtCore -from PySide2.QtWidgets import QSlider +from qtpy import QtCore +from qtpy.QtWidgets import QSlider class UISliderWidget(QSlider): diff --git a/eqt/ui/UIStackedWidget.py b/eqt/ui/UIStackedWidget.py index ba39374..f2bdb57 100644 --- a/eqt/ui/UIStackedWidget.py +++ b/eqt/ui/UIStackedWidget.py @@ -1,6 +1,6 @@ -from PySide2 import QtWidgets -from PySide2.QtCore import Qt -from PySide2.QtWidgets import QHBoxLayout, QListWidget, QStackedWidget, QVBoxLayout, QWidget +from qtpy import QtWidgets +from qtpy.QtCore import Qt +from qtpy.QtWidgets import QHBoxLayout, QListWidget, QStackedWidget, QVBoxLayout, QWidget from .UIFormWidget import UIFormFactory diff --git a/examples/MainWindowWithSessionManagement_example.py b/examples/MainWindowWithSessionManagement_example.py index ff52f45..91d138d 100644 --- a/examples/MainWindowWithSessionManagement_example.py +++ b/examples/MainWindowWithSessionManagement_example.py @@ -1,7 +1,7 @@ import sys -from PySide2 import QtWidgets -from PySide2.QtWidgets import QApplication +from qtpy import QtWidgets +from qtpy.QtWidgets import QApplication from eqt import __version__ from eqt.ui.MainWindowWithSessionManagement import MainWindowWithSessionManagement diff --git a/examples/NoBorderScrollArea_example.py b/examples/NoBorderScrollArea_example.py index ab13b07..a71c2ee 100644 --- a/examples/NoBorderScrollArea_example.py +++ b/examples/NoBorderScrollArea_example.py @@ -1,7 +1,7 @@ import sys -from PySide2 import QtWidgets -from PySide2.QtWidgets import QPushButton +from qtpy import QtWidgets +from qtpy.QtWidgets import QPushButton from eqt.ui.NoBorderScrollArea import NoBorderScrollArea diff --git a/examples/advanced_dialog_example.py b/examples/advanced_dialog_example.py index ea59e6a..8f451d3 100644 --- a/examples/advanced_dialog_example.py +++ b/examples/advanced_dialog_example.py @@ -1,7 +1,7 @@ import sys import utilitiesForExamples -from PySide2 import QtWidgets +from qtpy import QtWidgets from eqt.ui.FormDialog import AdvancedFormDialog from eqt.ui.UIFormWidget import FormWidget diff --git a/examples/dialog_example.py b/examples/dialog_example.py index b1b3dc0..1f6cf74 100644 --- a/examples/dialog_example.py +++ b/examples/dialog_example.py @@ -1,6 +1,6 @@ import sys -from PySide2 import QtWidgets +from qtpy import QtWidgets from eqt.ui import UIFormFactory diff --git a/examples/dialog_example_2.py b/examples/dialog_example_2.py index 4dec5e0..a0c736c 100644 --- a/examples/dialog_example_2.py +++ b/examples/dialog_example_2.py @@ -1,6 +1,6 @@ import sys -from PySide2 import QtWidgets +from qtpy import QtWidgets from eqt.ui import FormDialog diff --git a/examples/dialog_example_3_save_default.py b/examples/dialog_example_3_save_default.py index 27c8cc5..faa3f30 100644 --- a/examples/dialog_example_3_save_default.py +++ b/examples/dialog_example_3_save_default.py @@ -1,7 +1,7 @@ import sys import utilitiesForExamples as utex -from PySide2 import QtWidgets +from qtpy import QtWidgets from eqt.ui import FormDialog diff --git a/examples/dialog_multistep_example.py b/examples/dialog_multistep_example.py index 5b4aac0..fd1a7e0 100644 --- a/examples/dialog_multistep_example.py +++ b/examples/dialog_multistep_example.py @@ -1,6 +1,6 @@ import sys -from PySide2 import QtWidgets +from qtpy import QtWidgets from eqt.ui import UIFormFactory, UIMultiStepFactory diff --git a/examples/dialog_save_state_example.py b/examples/dialog_save_state_example.py index d0ccf2c..3f3e54d 100644 --- a/examples/dialog_save_state_example.py +++ b/examples/dialog_save_state_example.py @@ -1,6 +1,6 @@ import sys -from PySide2 import QtWidgets +from qtpy import QtWidgets from eqt.ui import FormDialog from eqt.ui.UISliderWidget import UISliderWidget @@ -35,7 +35,7 @@ def __init__(self, parent=None): dialog.addWidget(QtWidgets.QSlider(), 'Slider: ', 'slider') dialog.addWidget(UISliderWidget(QtWidgets.QLabel()), 'UISliderWidget: ', 'uiSliderWidget') dialog.addWidget(QtWidgets.QRadioButton('test 1'), 'RadioButton 1: ', 'radioButton') - dialog.addWidget(QtWidgets.QRadioButton('test 2'), 'RadioButton 2: ', 'radioButton') + dialog.addWidget(QtWidgets.QRadioButton('test 2'), 'RadioButton 2: ', 'radioButton_2') dialog.addWidget(QtWidgets.QTextEdit('test'), 'TextEdit: ', 'textEdit') dialog.addWidget(QtWidgets.QPlainTextEdit('test'), 'PlainTextEdit: ', 'plainTextEdit') dialog.addWidget(QtWidgets.QLineEdit('test'), 'LineEdit: ', 'lineEdit') diff --git a/examples/insert_widgets_example.py b/examples/insert_widgets_example.py index 29e8cba..c5ddda1 100644 --- a/examples/insert_widgets_example.py +++ b/examples/insert_widgets_example.py @@ -1,6 +1,6 @@ import sys -from PySide2 import QtWidgets +from qtpy import QtWidgets from eqt.ui import FormDialog, UIFormWidget diff --git a/examples/progress_timer_dialog_example.py b/examples/progress_timer_dialog_example.py index 675fcce..be2666b 100644 --- a/examples/progress_timer_dialog_example.py +++ b/examples/progress_timer_dialog_example.py @@ -1,8 +1,8 @@ import sys from time import sleep -from PySide2 import QtCore, QtWidgets -from PySide2.QtCore import QThreadPool +from qtpy import QtCore, QtWidgets +from qtpy.QtCore import QThreadPool from eqt.threading import Worker from eqt.ui import ProgressTimerDialog diff --git a/examples/remove_widgets_example.py b/examples/remove_widgets_example.py index 74e0b57..8c4933f 100644 --- a/examples/remove_widgets_example.py +++ b/examples/remove_widgets_example.py @@ -1,6 +1,6 @@ import sys -from PySide2 import QtWidgets +from qtpy import QtWidgets from eqt.ui import FormDialog, UIFormWidget diff --git a/examples/reorderable_list_widget_example.py b/examples/reorderable_list_widget_example.py index 58f12c4..73f2d2f 100644 --- a/examples/reorderable_list_widget_example.py +++ b/examples/reorderable_list_widget_example.py @@ -1,8 +1,8 @@ import sys import qdarkstyle -from PySide2 import QtWidgets from qdarkstyle.dark.palette import DarkPalette +from qtpy import QtWidgets from eqt.ui.ReOrderableListWidget import ReOrderableListWidget diff --git a/examples/utilitiesForExamples.py b/examples/utilitiesForExamples.py index 4f409f5..b4c8c9d 100644 --- a/examples/utilitiesForExamples.py +++ b/examples/utilitiesForExamples.py @@ -1,4 +1,4 @@ -from PySide2 import QtWidgets +from qtpy import QtWidgets from eqt.ui.UISliderWidget import UISliderWidget diff --git a/pyproject.toml b/pyproject.toml index d22dca0..3082427 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3 :: Only"] -dependencies = ["pyside2", "qdarkstyle"] +dependencies = ["qtpy", "qdarkstyle"] [project.optional-dependencies] dev = ["pytest>=6", "pytest-cov", "pytest-timeout"] diff --git a/recipe/eqt_env.yml b/recipe/eqt_env.yml index f40ad8d..f4d27f3 100644 --- a/recipe/eqt_env.yml +++ b/recipe/eqt_env.yml @@ -4,5 +4,5 @@ channels: dependencies: - python <3.12 - pip - - pyside2 + - qtpy - qdarkstyle diff --git a/recipe/pyqt_env.yml b/recipe/pyqt_env.yml new file mode 100644 index 0000000..8486f88 --- /dev/null +++ b/recipe/pyqt_env.yml @@ -0,0 +1,7 @@ +name: eqt_env +channels: + - conda-forge +dependencies: + - pip + - pip: + - PyQt5 diff --git a/recipe/pyside_env.yml b/recipe/pyside_env.yml new file mode 100644 index 0000000..657260e --- /dev/null +++ b/recipe/pyside_env.yml @@ -0,0 +1,5 @@ +name: eqt_env +channels: + - conda-forge +dependencies: + - pyside2 diff --git a/test/__init__.py b/test/__init__.py index bb1eb5b..9bbd41c 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,7 +1,7 @@ import os -from PySide2 import QtWidgets from pytest import skip +from qtpy import QtWidgets from eqt.ui import FormDialog diff --git a/test/dialog_example_2_test.py b/test/dialog_example_2_test.py index d15e7f1..61f3d9d 100644 --- a/test/dialog_example_2_test.py +++ b/test/dialog_example_2_test.py @@ -1,9 +1,9 @@ import sys import unittest -from PySide2 import QtWidgets -from PySide2.QtCore import Qt -from PySide2.QtTest import QTest +from qtpy import QtWidgets +from qtpy.QtCore import Qt +from qtpy.QtTest import QTest from eqt.ui import FormDialog diff --git a/test/test_MainWindowWithSessionManagement.py b/test/test_MainWindowWithSessionManagement.py index 7614b14..d21a410 100644 --- a/test/test_MainWindowWithSessionManagement.py +++ b/test/test_MainWindowWithSessionManagement.py @@ -6,8 +6,8 @@ from unittest import mock from unittest.mock import patch -from PySide2.QtCore import QSettings, QThreadPool -from PySide2.QtWidgets import QMenu, QMenuBar +from qtpy.QtCore import QSettings, QThreadPool +from qtpy.QtWidgets import QMenuBar import eqt from eqt.io import zip_directory @@ -102,8 +102,6 @@ def test_createMenu_sets_menu_bar_and_menus(self): # dict should contain the expected menus assert "File" in self.smw.menus assert "Settings" in self.smw.menus - assert isinstance(self.smw.menus["File"], QMenu) - assert isinstance(self.smw.menus["Settings"], QMenu) def test_menu_has_file_and_settings_menu(self): actions = self.smw.menu_bar.actions() @@ -111,15 +109,16 @@ def test_menu_has_file_and_settings_menu(self): assert actions[1].text() == "Settings" def test_file_menu_has_expected_actions(self): - menus = self.smw.menu_bar.findChildren(QMenu) - file_menu = menus[0] + "Extracts the nested menus" + menus = self.smw.menu_bar.actions() + file_menu = menus[0].menu() assert file_menu.actions()[0].text() == "Save" assert file_menu.actions()[1].text() == "Save + Exit" assert file_menu.actions()[2].text() == "Exit" def test_settings_menu_has_expected_actions(self): - menus = self.smw.menu_bar.findChildren(QMenu) - settings_menu = menus[1] + menus = self.smw.menu_bar.actions() + settings_menu = menus[1].menu() self.assertEqual(settings_menu.actions()[0].text(), "App Settings") self.assertEqual(settings_menu.actions()[1].text(), "Set Session Directory") diff --git a/test/test_NoBorderScrollArea.py b/test/test_NoBorderScrollArea.py index d3d21c0..643e9cc 100644 --- a/test/test_NoBorderScrollArea.py +++ b/test/test_NoBorderScrollArea.py @@ -1,6 +1,6 @@ import unittest -from PySide2.QtWidgets import QHBoxLayout, QPushButton, QWidget +from qtpy.QtWidgets import QHBoxLayout, QPushButton, QWidget from eqt.ui.NoBorderScrollArea import NoBorderScrollArea diff --git a/test/test_SessionDialogs.py b/test/test_SessionDialogs.py index 5a3c352..20fa03a 100644 --- a/test/test_SessionDialogs.py +++ b/test/test_SessionDialogs.py @@ -3,7 +3,7 @@ from pathlib import Path from unittest.mock import patch -from PySide2.QtWidgets import QFileDialog +from qtpy.QtWidgets import QFileDialog from eqt.ui.SessionDialogs import ( AppSettingsDialog, @@ -83,19 +83,17 @@ def test_select_session_directory_label_when_app_name_set(self): sdsd.getWidget("select_session_directory").text(), "Select a session directory to save and retrieve all Test App Sessions:") - @patch("PySide2.QtWidgets.QFileDialog.getExistingDirectory") + @patch("qtpy.QtWidgets.QFileDialog.getExistingDirectory") def test_browse_for_dir_button_makes_file_dialog_for_getting_dir(self, mock_dialog_call): sdsd = SessionDirectorySelectionDialog() sdsd.browse_for_dir() mock_dialog_call.assert_called_once() - @patch("PySide2.QtWidgets.QFileDialog.getExistingDirectory") - def test_browse_button_calls_browse_for_dir(self, mock_dialog_call): + @patch.object(SessionDirectorySelectionDialog, "browse_for_dir") + def test_browse_button_calls_browse_for_dir(self, mock_browse): sdsd = SessionDirectorySelectionDialog() - sdsd.browse_for_dir = unittest.mock.Mock() - QFileDialog.getExistingDirectory = unittest.mock.Mock() sdsd.getWidget("selected_dir").click() - sdsd.browse_for_dir.assert_called_once() + mock_browse.assert_called_once() def test_browse_dialog_updates_session_directory_label(self): example_dir = "C:\\Users\\test_user\\Documents\\test_dir" diff --git a/test/test__formUI_status_test.py b/test/test__formUI_status_test.py index f5600e7..ebae54d 100644 --- a/test/test__formUI_status_test.py +++ b/test/test__formUI_status_test.py @@ -2,9 +2,9 @@ import unittest from unittest import mock -from PySide2 import QtWidgets -from PySide2.QtCore import Qt -from PySide2.QtTest import QTest +from qtpy import QtWidgets +from qtpy.QtCore import Qt +from qtpy.QtTest import QTest from eqt.ui.FormDialog import AdvancedFormDialog, FormDialog from eqt.ui.UIFormWidget import FormDockWidget, FormWidget