Skip to content

Commit 19be963

Browse files
paskinoDanicaSTFC
andauthored
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 <[email protected]> Danica Sugic <[email protected]>
1 parent 6905536 commit 19be963

40 files changed

+179
-144
lines changed

.github/workflows/test.yml

+3
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@ jobs:
1010
strategy:
1111
matrix:
1212
python: [3.8, 3.11]
13+
qtbindings: ['PySide2', 'PyQt5']
1314
steps:
1415
- uses: actions/checkout@v4
1516
with:
1617
fetch-depth: 0
1718
- uses: actions/setup-python@v5
1819
with:
1920
python-version: ${{ matrix.python }}
21+
qt-bindings: ${{ matrix.qtbindings }}
2022
- run: pip install -U .[dev]
23+
- run: pip install ${{ matrix.qtbindings }}
2124
- run: pytest
2225
deploy:
2326
needs: [test]

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# Version 2.0.0
2+
- Use `qtpy` as virtual Qt binding package. GHA unit tests are run with PySide2 and PyQt5 (#146)
3+
- Add `pyqt_env.yml` and `pyside_env.yml` environment files (#146)
4+
- Update `CONTRIBUTING.md`, `README.md` and add documentation file (#146)
5+
16
# Version 1.0.2
27
- Upgrade python to 3.8 in `test.yml` (#171)
38
- Rename `/scripts` directory to `/recipe` (#161)

CONTRIBUTING.md

+9
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ cd eqt
3232
mamba env create -f recipe/eqt_env.yml
3333
```
3434

35+
`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.
36+
```sh
37+
mamba env update --name eqt_env --file pyside_env.yml
38+
```
39+
or
40+
```sh
41+
mamba env update --name eqt_env --file pyqt_env.yml
42+
```
43+
3544
4. Activate the environment:
3645
```sh
3746
mamba activate eqt_env

Documentation.md

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Documentation for eqt
2+
3+
## Running asynchronous tasks
4+
5+
To run a function in a separate thread we use a `Worker` which is a subclass of a `QRunnable`.
6+
7+
For the `Worker` to work one needs to define:
8+
9+
1. the function that does what you need
10+
2. Optional callback methods to get the status of the thread by means of `QtCore.QSignal`s
11+
12+
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`.
13+
14+
```python
15+
class Worker(QtCore.QRunnable):
16+
def __init__(self, fn, *args, **kwargs):
17+
self.fn = fn
18+
self.args = args
19+
self.kwargs = kwargs
20+
self.signals = WorkerSignals()
21+
```
22+
23+
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.
24+
25+
```python
26+
result = self.fn(*self.args, **self.kwargs)
27+
```
28+
29+
But `Worker` will [add](https://github.com/TomographicImaging/eqt/blob/535e487d09d928713d7d6aa1123657597627c4b0/eqt/threading/QtThreading.py#L41-L43) to the `**kwargs` the following `QSignal`.
30+
31+
```python
32+
# Add progress callback to kwargs
33+
self.kwargs['progress_callback'] = self.signals.progress
34+
self.kwargs['message_callback'] = self.signals.message
35+
self.kwargs['status_callback'] = self.signals.status
36+
```
37+
38+
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:
39+
40+
```python
41+
def fn(num_iter, **kwargs):
42+
progress_callback = kwargs.get('progress_callback', None)
43+
for i in range(num_iter):
44+
do_something
45+
if progress_callback is not None:
46+
progress_callback.emit( i )
47+
```
48+
49+
### Passing a signal to a Worker
50+
51+
This is done just after one has defined the `Worker`:
52+
53+
```python
54+
def handle_progress(num_iter):
55+
# do something with the progress
56+
print ("Current progress is ", num_iter)
57+
58+
worker = Worker(fn, 10)
59+
worker.signals.progress.connect(handle_progress)
60+
```
61+
62+
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.
63+
64+
### Signals available
65+
66+
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.
67+
68+
```python
69+
finished = QtCore.Signal()
70+
error = QtCore.Signal(tuple)
71+
result = QtCore.Signal(object)
72+
73+
progress = QtCore.Signal(int)
74+
message = QtCore.Signal(str)
75+
status = QtCore.Signal(tuple)
76+
```
77+
78+
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).

README.md

+9-78
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
Templates & tools to develop Qt GUIs in Python.
66

7-
One use case is accepting user input while running another task asynchronously (so that the UI is still responsive).
8-
7+
Some example classes are
98
1. `UIFormWidget`: a class to help creating Qt forms programmatically, useable in `QDockWidgets` and `QWidget`
109
2. `FormDialog`: a `QDialog` with a form inside with <kbd>OK</kbd> and <kbd>Cancel</kbd> buttons
1110
3. `Worker`: a class that defines a `QRunnable` to handle worker thread setup, signals and wrap up
1211

12+
One use case is accepting a user input while running another task asynchronously (so that the UI is still responsive).
13+
1314
## Installation
1415

1516
Via `pip`/`conda`/`mamba`, i.e. any of the following:
@@ -18,86 +19,16 @@ Via `pip`/`conda`/`mamba`, i.e. any of the following:
1819
- `conda install -c conda-forge eqt`
1920
- `mamba install -c conda-forge eqt`
2021

21-
## Examples
22-
23-
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).
24-
25-
### Running asynchronous tasks
26-
27-
To run a function in a separate thread we use a `Worker` which is a subclass of a `QRunnable`.
28-
29-
For the `Worker` to work one needs to define:
30-
31-
1. the function that does what you need
32-
2. Optional callback methods to get the status of the thread by means of `QtCore.QSignal`s
33-
34-
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`.
35-
36-
```python
37-
class Worker(QtCore.QRunnable):
38-
def __init__(self, fn, *args, **kwargs):
39-
self.fn = fn
40-
self.args = args
41-
self.kwargs = kwargs
42-
self.signals = WorkerSignals()
43-
```
44-
45-
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.
46-
47-
```python
48-
result = self.fn(*self.args, **self.kwargs)
49-
```
5022

51-
But `Worker` will [add](https://github.com/TomographicImaging/eqt/blob/535e487d09d928713d7d6aa1123657597627c4b0/eqt/threading/QtThreading.py#L41-L43) to the `**kwargs` the following `QSignal`.
23+
#### Note:
24+
`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`.
5225

53-
```python
54-
# Add progress callback to kwargs
55-
self.kwargs['progress_callback'] = self.signals.progress
56-
self.kwargs['message_callback'] = self.signals.message
57-
self.kwargs['status_callback'] = self.signals.status
58-
```
59-
60-
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:
61-
62-
```python
63-
def fn(num_iter, **kwargs):
64-
progress_callback = kwargs.get('progress_callback', None)
65-
for i in range(num_iter):
66-
do_something
67-
if progress_callback is not None:
68-
progress_callback.emit( i )
69-
```
70-
71-
### Passing a signal to a Worker
72-
73-
This is done just after one has defined the `Worker`:
74-
75-
```python
76-
def handle_progress(num_iter):
77-
# do something with the progress
78-
print ("Current progress is ", num_iter)
79-
80-
worker = Worker(fn, 10)
81-
worker.signals.progress.connect(handle_progress)
82-
```
83-
84-
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.
85-
86-
### Signals available
87-
88-
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.
89-
90-
```python
91-
finished = QtCore.Signal()
92-
error = QtCore.Signal(tuple)
93-
result = QtCore.Signal(object)
26+
## Examples
9427

95-
progress = QtCore.Signal(int)
96-
message = QtCore.Signal(str)
97-
status = QtCore.Signal(tuple)
98-
```
28+
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).
9929

100-
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).
30+
## Documentation
31+
See [Documentation.md](./Documentation.md).
10132

10233
## Developer Contribution Guide
10334
See [CONTRIBUTING.md](./CONTRIBUTING.md).

eqt/threading/QtThreading.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
# https://www.geeksforgeeks.org/migrate-pyqt5-app-to-pyside2
88
import traceback
99

10-
from PySide2 import QtCore
11-
from PySide2.QtCore import Slot
10+
from qtpy import QtCore
11+
from qtpy.QtCore import Slot
1212

1313

1414
class Worker(QtCore.QRunnable):

eqt/ui/FormDialog.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from PySide2 import QtCore, QtWidgets
1+
from qtpy import QtCore, QtWidgets
22

33
from . import UIFormFactory
44

eqt/ui/MainWindowWithProgressDialogs.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import qdarkstyle
2-
from PySide2.QtCore import QSettings, QThreadPool
3-
from PySide2.QtGui import QKeySequence
4-
from PySide2.QtWidgets import QAction, QMainWindow
52
from qdarkstyle.dark.palette import DarkPalette
63
from qdarkstyle.light.palette import LightPalette
4+
from qtpy.QtCore import QSettings, QThreadPool
5+
from qtpy.QtGui import QKeySequence
6+
from qtpy.QtWidgets import QAction, QMainWindow
77

88
from .ProgressTimerDialog import ProgressTimerDialog
99
from .SessionDialogs import AppSettingsDialog

eqt/ui/MainWindowWithSessionManagement.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
from datetime import datetime
55
from functools import partial
66

7-
from PySide2.QtGui import QCloseEvent, QKeySequence
8-
from PySide2.QtWidgets import QAction
7+
from qtpy.QtGui import QCloseEvent, QKeySequence
8+
from qtpy.QtWidgets import QAction
99

1010
from ..io import zip_directory
1111
from ..threading import Worker

eqt/ui/NoBorderScrollArea.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import re
22

33
import qdarkstyle
4-
from PySide2.QtWidgets import QPushButton, QScrollArea, QWidget
4+
from qtpy.QtWidgets import QPushButton, QScrollArea, QWidget
55

66

77
class NoBorderScrollArea(QScrollArea):

eqt/ui/ProgressTimerDialog.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import time
22
from time import sleep
33

4-
from PySide2 import QtCore
5-
from PySide2.QtCore import Qt, QThreadPool
6-
from PySide2.QtWidgets import QProgressDialog
4+
from qtpy import QtCore
5+
from qtpy.QtCore import Qt, QThreadPool
6+
from qtpy.QtWidgets import QProgressDialog
77

88
from ..threading import Worker
99

eqt/ui/ReOrderableListWidget.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from PySide2 import QtCore, QtWidgets
1+
from qtpy import QtCore, QtWidgets
22

33

44
class ReOrderableListWidget(QtWidgets.QTableWidget):

eqt/ui/SessionDialogs.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22

3-
from PySide2 import QtWidgets
4-
from PySide2.QtWidgets import (
3+
from qtpy import QtWidgets
4+
from qtpy.QtWidgets import (
55
QCheckBox,
66
QComboBox,
77
QFileDialog,

eqt/ui/UIFormWidget.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from warnings import warn
22

3-
from PySide2 import QtWidgets
3+
from qtpy import QtWidgets
44

55
from .UISliderWidget import UISliderWidget
66

eqt/ui/UIMultiStepWidget.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from PySide2 import QtWidgets
2-
from PySide2.QtCore import Qt
3-
from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QPushButton
1+
from qtpy import QtWidgets
2+
from qtpy.QtCore import Qt
3+
from qtpy.QtWidgets import QGroupBox, QHBoxLayout, QPushButton
44

55

66
class UIMultiStepWidget(object):

eqt/ui/UISliderWidget.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from PySide2 import QtCore
2-
from PySide2.QtWidgets import QSlider
1+
from qtpy import QtCore
2+
from qtpy.QtWidgets import QSlider
33

44

55
class UISliderWidget(QSlider):

eqt/ui/UIStackedWidget.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from PySide2 import QtWidgets
2-
from PySide2.QtCore import Qt
3-
from PySide2.QtWidgets import QHBoxLayout, QListWidget, QStackedWidget, QVBoxLayout, QWidget
1+
from qtpy import QtWidgets
2+
from qtpy.QtCore import Qt
3+
from qtpy.QtWidgets import QHBoxLayout, QListWidget, QStackedWidget, QVBoxLayout, QWidget
44

55
from .UIFormWidget import UIFormFactory
66

examples/MainWindowWithSessionManagement_example.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import sys
22

3-
from PySide2 import QtWidgets
4-
from PySide2.QtWidgets import QApplication
3+
from qtpy import QtWidgets
4+
from qtpy.QtWidgets import QApplication
55

66
from eqt import __version__
77
from eqt.ui.MainWindowWithSessionManagement import MainWindowWithSessionManagement

examples/NoBorderScrollArea_example.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import sys
22

3-
from PySide2 import QtWidgets
4-
from PySide2.QtWidgets import QPushButton
3+
from qtpy import QtWidgets
4+
from qtpy.QtWidgets import QPushButton
55

66
from eqt.ui.NoBorderScrollArea import NoBorderScrollArea
77

examples/advanced_dialog_example.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import sys
22

33
import utilitiesForExamples
4-
from PySide2 import QtWidgets
4+
from qtpy import QtWidgets
55

66
from eqt.ui.FormDialog import AdvancedFormDialog
77
from eqt.ui.UIFormWidget import FormWidget

examples/dialog_example.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import sys
22

3-
from PySide2 import QtWidgets
3+
from qtpy import QtWidgets
44

55
from eqt.ui import UIFormFactory
66

examples/dialog_example_2.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import sys
22

3-
from PySide2 import QtWidgets
3+
from qtpy import QtWidgets
44

55
from eqt.ui import FormDialog
66

examples/dialog_example_3_save_default.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import sys
22

33
import utilitiesForExamples as utex
4-
from PySide2 import QtWidgets
4+
from qtpy import QtWidgets
55

66
from eqt.ui import FormDialog
77

0 commit comments

Comments
 (0)