Skip to content

Commit 8644d63

Browse files
authored
Merge pull request #493 from nicoddemus/docs-methods-vs-clicks
Docs: suggest direct methods instead of qtbot's
2 parents 441e7a6 + 5a75583 commit 8644d63

File tree

7 files changed

+92
-52
lines changed

7 files changed

+92
-52
lines changed

.github/workflows/main.yml

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ name: build
22

33
on: [push, pull_request]
44

5+
# Cancel running jobs for the same workflow and branch.
6+
concurrency:
7+
group: ${{ github.workflow }}-${{ github.ref }}
8+
cancel-in-progress: true
9+
510
jobs:
611
build:
712

@@ -12,7 +17,7 @@ jobs:
1217
matrix:
1318
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
1419
qt-lib: [pyqt5, pyqt6, pyside2, pyside6]
15-
os: [ubuntu-20.04, windows-latest, macos-latest]
20+
os: [ubuntu-latest, windows-latest, macos-latest]
1621
include:
1722
- python-version: "3.7"
1823
tox-env: "py37"
@@ -24,20 +29,12 @@ jobs:
2429
tox-env: "py310"
2530
- python-version: "3.11"
2631
tox-env: "py311"
27-
# https://bugreports.qt.io/browse/PYSIDE-1797
2832
exclude:
29-
- qt-lib: pyside6
30-
os: macos-latest
31-
python-version: "3.7"
32-
- qt-lib: pyside6
33-
os: ubuntu-20.04
34-
python-version: "3.7"
35-
# Not installable so far
36-
- qt-lib: pyside6
37-
python-version: "3.11"
38-
- qt-lib: pyside2
33+
# Not installable:
34+
# ERROR: Could not find a version that satisfies the requirement pyside2 (from versions: none)
35+
- python-version: "3.11"
36+
qt-lib: pyside2
3937
os: windows-latest
40-
python-version: "3.11"
4138

4239
steps:
4340
- uses: actions/checkout@v3
@@ -50,7 +47,7 @@ jobs:
5047
run: |
5148
python -m pip install --upgrade pip
5249
pip install tox
53-
if [ "${{ matrix.os }}" == "ubuntu-20.04" ]; then
50+
if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then
5451
sudo apt-get update -y
5552
sudo apt-get install -y libgles2-mesa-dev
5653
fi

docs/intro.rst

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ pytest-qt is a `pytest`_ plugin that allows programmers to write tests
66
for `PyQt5`_, `PyQt6`_, `PySide2`_ and `PySide6`_ applications.
77

88
The main usage is to use the ``qtbot`` fixture, responsible for handling ``qApp``
9-
creation as needed and provides methods to simulate user interaction,
10-
like key presses and mouse clicks:
9+
creation as needed, and registering widgets for testing:
1110

1211

1312
.. code-block:: python
@@ -16,8 +15,8 @@ like key presses and mouse clicks:
1615
widget = HelloWidget()
1716
qtbot.addWidget(widget)
1817
19-
# click in the Greet button and make sure it updates the appropriate label
20-
qtbot.mouseClick(widget.button_greet, QtCore.Qt.LeftButton)
18+
# Click the greet button and make sure the appropriate label is updated.
19+
widget.button_greet.click()
2120
2221
assert widget.greet_label.text() == "Hello!"
2322

docs/note_dialogs.rst

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,14 @@ And now it is also easy to mock ``AskNameAndAgeDialog.ask`` when testing the for
4949
.. code-block:: python
5050
5151
def test_form_registration(qtbot, monkeypatch):
52-
form = RegistrationForm()
52+
user = User.empty_user()
53+
form = RegistrationForm(user)
5354
5455
monkeypatch.setattr(
55-
AskNameAndAgeDialog, "ask", classmethod(lambda *args: ("Jonh", 30))
56+
AskNameAndAgeDialog, "ask", classmethod(lambda *args: ("John", 30))
5657
)
57-
qtbot.click(form.enter_info())
58-
# calls AskNameAndAgeDialog.ask
59-
# test that the rest of the form correctly behaves as if
60-
# user entered "Jonh" and 30 as name and age
58+
# Clicking on the button will call AskNameAndAgeDialog.ask in its slot.
59+
form.enter_info_button.click()
60+
61+
assert user.name == "John"
62+
assert user.age == 30

docs/qapplication.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ For example:
2626
exit_calls = []
2727
monkeypatch.setattr(QApplication, "exit", lambda: exit_calls.append(1))
2828
button = get_app_exit_button()
29-
qtbot.click(button)
29+
button.click()
3030
assert exit_calls == [1]
3131
3232
@@ -37,7 +37,7 @@ Or using the ``mock`` package:
3737
def test_exit_button(qtbot):
3838
with mock.patch.object(QApplication, "exit"):
3939
button = get_app_exit_button()
40-
qtbot.click(button)
40+
button.click()
4141
assert QApplication.exit.call_count == 1
4242
4343

docs/tutorial.rst

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,29 @@ to search for and a button to browse for the desired directory. Its source code
1818

1919
.. _here: https://github.com/nicoddemus/PySide-Examples/blob/master/examples/dialogs/findfiles.py
2020

21-
To test this widget's basic functionality, create a test function::
21+
To test this widget's basic functionality, create a test function:
2222

23-
def test_basic_search(qtbot, tmpdir):
24-
'''
23+
.. code-block:: python
24+
25+
def test_basic_search(qtbot, tmp_path):
26+
"""
2527
test to ensure basic find files functionality is working.
26-
'''
27-
tmpdir.join('video1.avi').ensure()
28-
tmpdir.join('video1.srt').ensure()
28+
"""
29+
tmp_path.joinpath("video1.avi").touch()
30+
tmp_path.joinpath("video1.srt").touch()
2931
30-
tmpdir.join('video2.avi').ensure()
31-
tmpdir.join('video2.srt').ensure()
32+
tmp_path.joinpath("video2.avi").touch()
33+
tmp_path.joinpath("video2.srt").touch()
3234
3335
Here the first parameter indicates that we will be using a ``qtbot`` fixture to control our widget.
3436
The other parameter is pytest's standard tmpdir_ that we use to create some files that will be
3537
used during our test.
3638

3739
.. _tmpdir: http://pytest.org/latest/tmpdir.html
3840

39-
Now we create the widget to test and register it::
41+
Now we create the widget to test and register it:
42+
43+
.. code-block:: python
4044
4145
window = Window()
4246
window.show()
@@ -45,24 +49,50 @@ Now we create the widget to test and register it::
4549
.. tip:: Registering widgets is not required, but recommended because it will ensure those widgets get
4650
properly closed after each test is done.
4751

48-
Now we use ``qtbot`` methods to simulate user interaction with the dialog::
52+
Now we can interact with the widgets directly:
53+
54+
.. code-block:: python
4955
5056
window.fileComboBox.clear()
51-
qtbot.keyClicks(window.fileComboBox, '*.avi')
57+
window.fileComboBox.setCurrentText("*.avi")
5258
5359
window.directoryComboBox.clear()
54-
qtbot.keyClicks(window.directoryComboBox, str(tmpdir))
60+
window.directoryComboBox.setCurrentText(str(tmp_path))
61+
62+
63+
We use the ``QComboBox.setCurrentText`` method to change the current item selected in the combo box.
64+
65+
66+
.. _note-about-qtbot-methods:
5567

56-
The method ``keyClicks`` is used to enter text in the editable combo box, selecting the desired mask
57-
and directory.
68+
.. note::
5869

59-
We then simulate a user clicking the button with the ``mouseClick`` method::
70+
In general, prefer to use a widget's own methods to interact with it: ``QComboBox.setCurrentIndex``, ``QLineEdit.setText``,
71+
etc. Those methods will emit the appropriate signal, so the test will work just the same as if the user themselves
72+
have interacted with the controls.
6073

61-
qtbot.mouseClick(window.findButton, QtCore.Qt.LeftButton)
74+
Note that ``qtbot`` provides a number of methods to simulate actual interaction, for example ``keyClicks``, ``mouseClick``,
75+
etc. Those methods should be used only in specialized situations, for example if you are creating a custom drawing widget
76+
and want to simulate actual clicks.
77+
78+
For normal interactions, always prefer widget methods (``setCurrentIndex``, ``setText``, etc) -- ``qtbot``'s methods
79+
(``keyClicks``, ``mouseClick``, etc) will trigger an actual event, which will then need to be processed in the next
80+
pass of the event loop, making the test unreliable and flaky. Also some operations are hard to simulate using
81+
raw clicks, for example selecting an item on a ``QComboBox``, which will need two ``mouseClick``
82+
calls to simulate properly, while figuring out where to click.
83+
84+
85+
We then simulate a user clicking the button:
86+
87+
.. code-block:: python
88+
89+
window.findButton.click()
6290
6391
Once this is done, we inspect the results widget to ensure that it contains the expected files we
64-
created earlier::
92+
created earlier:
93+
94+
.. code-block:: python
6595
6696
assert window.filesTable.rowCount() == 2
67-
assert window.filesTable.item(0, 0).text() == 'video1.avi'
68-
assert window.filesTable.item(1, 0).text() == 'video2.avi'
97+
assert window.filesTable.item(0, 0).text() == "video1.avi"
98+
assert window.filesTable.item(1, 0).text() == "video2.avi"

docs/virtual_methods.rst

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ It is common in Qt programming to override virtual C++ methods to customize
77
behavior, like listening for mouse events, implement drawing routines, etc.
88

99
Fortunately, all Python bindings for Qt support overriding these virtual methods
10-
naturally in your Python code::
10+
naturally in your Python code:
1111

12-
class MyWidget(QWidget):
12+
.. code-block:: python
1313
14+
class MyWidget(QWidget):
1415
# mouseReleaseEvent
1516
def mouseReleaseEvent(self, ev):
16-
print('mouse released at: %s' % ev.pos())
17+
print(f"mouse released at: {ev.pos()}")
1718
1819
In ``PyQt5`` and ``PyQt6``, exceptions in virtual methods will by default call
1920
abort(), which will crash the interpreter. All other Qt wrappers will print the
@@ -22,12 +23,14 @@ value is required).
2223

2324
This might be surprising for Python users which are used to exceptions
2425
being raised at the calling point: For example, the following code will just
25-
print a stack trace without raising any exception::
26+
print a stack trace without raising any exception:
2627

27-
class MyWidget(QWidget):
28+
.. code-block:: python
2829
30+
class MyWidget(QWidget):
2931
def mouseReleaseEvent(self, ev):
30-
raise RuntimeError('unexpected error')
32+
raise RuntimeError("unexpected error")
33+
3134
3235
w = MyWidget()
3336
QTest.mouseClick(w, QtCore.Qt.LeftButton)
@@ -49,7 +52,9 @@ Disabling the automatic exception hook
4952
--------------------------------------
5053

5154
You can disable the automatic exception hook on individual tests by using a
52-
``qt_no_exception_capture`` marker::
55+
``qt_no_exception_capture`` marker:
56+
57+
.. code-block:: python
5358
5459
@pytest.mark.qt_no_exception_capture
5560
def test_buttons(qtbot):

src/pytestqt/qtbot.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ class QtBot:
5555
Those methods are just forwarded directly to the `QTest API`_. Consult the documentation for more
5656
information.
5757
58+
.. note::
59+
These methods should be rarely be used, in general prefer to interact with widgets
60+
using their own methods such as ``QComboBox.setCurrentText``, ``QLineEdit.setText``, etc.
61+
Doing so will have the same effect as users interacting with the widget, but are more reliable.
62+
63+
See :ref:`this note in the tutorial <note-about-qtbot-methods>` for more information.
64+
5865
---
5966
6067
Below are methods used to simulate sending key events to widgets:

0 commit comments

Comments
 (0)