Skip to content

Commit a109e7f

Browse files
Add qt6 support
1 parent e30a282 commit a109e7f

11 files changed

Lines changed: 120 additions & 85 deletions

File tree

.github/workflows/ci.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,16 @@ jobs:
111111
matrix:
112112
python: ['3.10', '3.11']
113113
arch: ['x86', 'x64']
114-
qt_library: ['PyQt5', 'PySide2']
114+
qt_library: ['PyQt5', 'PySide2', 'PyQt6', 'PySide6']
115115
exclude:
116116
- python: '3.10'
117117
qt_library: 'PySide2'
118118
- python: '3.11'
119119
qt_library: 'PySide2'
120+
- arch: 'x86'
121+
qt_library: 'PyQt6'
122+
- arch: 'x86'
123+
qt_library: 'PySide6'
120124
steps:
121125
- name: Checkout
122126
uses: actions/checkout@v2
@@ -148,7 +152,7 @@ jobs:
148152
fail-fast: false
149153
matrix:
150154
python: ['3.10', '3.11']
151-
qt_library: ['PyQt5', 'PySide2']
155+
qt_library: ['PyQt5', 'PySide2', 'PyQt6', 'PySide6']
152156
exclude:
153157
- python: '3.10'
154158
qt_library: 'PySide2'
@@ -164,7 +168,7 @@ jobs:
164168
- name: Install Linux test dependencies
165169
run: |
166170
apt-get update --yes
167-
apt-get install --yes libgl1-mesa-dev xvfb x11-utils libdbus-1-3 libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0
171+
apt-get install --yes libgl1-mesa-dev xvfb x11-utils libdbus-1-3 libxkbcommon-x11-0 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0
168172
- name: Run tests
169173
run: ./ci.sh
170174
env:
@@ -184,7 +188,7 @@ jobs:
184188
fail-fast: false
185189
matrix:
186190
python: ['3.10', '3.11']
187-
qt_library: ['PyQt5', 'PySide2']
191+
qt_library: ['PyQt5', 'PySide2', 'PyQt6', 'PySide6']
188192
exclude:
189193
- python: '3.10'
190194
qt_library: 'PySide2'
@@ -201,6 +205,7 @@ jobs:
201205
run: ./ci.sh
202206
env:
203207
INSTALL_EXTRAS: '[${{ matrix.qt_library }},p-tests]'
208+
QT_QPA_PLATFORM: 'offscreen'
204209
- uses: codecov/codecov-action@v7
205210
if: always()
206211
with:

qtrio/_core.py

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import attr
1717
import outcome
1818
import qts
19+
import qts.util
1920
import trio
2021
import trio.abc
2122

@@ -65,16 +66,12 @@ def register_event_type() -> None:
6566

6667
# assign to the global
6768
# TODO: https://bugreports.qt.io/browse/PYSIDE-1347
68-
if qts.is_pyqt_5_wrapper:
69-
_reenter_event_type = QtCore.QEvent.Type(event_hint)
70-
elif qts.is_pyside_5_wrapper:
69+
if qts.is_pyside_5_wrapper:
7170
_reenter_event_type = typing.cast(
7271
typing.Callable[[int], QtCore.QEvent.Type], QtCore.QEvent.Type
7372
)(event_hint)
74-
else: # pragma: no cover
75-
raise qtrio.InternalError(
76-
"You should not be here but you are running neither PyQt5 nor PySide2.",
77-
)
73+
else:
74+
_reenter_event_type = QtCore.QEvent.Type(event_hint)
7875

7976

8077
def register_requested_event_type(requested_value: int | QtCore.QEvent.Type) -> None:
@@ -99,17 +96,13 @@ def register_requested_event_type(requested_value: int | QtCore.QEvent.Type) ->
9996
raise qtrio.EventTypeAlreadyRegisteredError()
10097

10198
# TODO: https://bugreports.qt.io/browse/PYSIDE-1468
102-
if qts.is_pyqt_5_wrapper:
103-
event_hint = QtCore.QEvent.registerEventType(requested_value)
104-
elif qts.is_pyside_5_wrapper:
99+
if qts.is_pyside_5_wrapper:
105100
event_hint = typing.cast(
106101
typing.Callable[[int | QtCore.QEvent.Type], int],
107102
QtCore.QEvent.registerEventType,
108103
)(requested_value)
109-
else: # pragma: no cover
110-
raise qtrio.InternalError(
111-
"You should not be here but you are running neither PyQt5 nor PySide2.",
112-
)
104+
else:
105+
event_hint = QtCore.QEvent.registerEventType(requested_value)
113106

114107
if event_hint == -1:
115108
raise qtrio.EventTypeRegistrationFailedError()
@@ -120,16 +113,12 @@ def register_requested_event_type(requested_value: int | QtCore.QEvent.Type) ->
120113

121114
# assign to the global
122115
# TODO: https://bugreports.qt.io/browse/PYSIDE-1347
123-
if qts.is_pyqt_5_wrapper:
124-
_reenter_event_type = QtCore.QEvent.Type(event_hint)
125-
elif qts.is_pyside_5_wrapper:
116+
if qts.is_pyside_5_wrapper:
126117
_reenter_event_type = typing.cast(
127118
typing.Callable[[int], QtCore.QEvent.Type], QtCore.QEvent.Type
128119
)(event_hint)
129-
else: # pragma: no cover
130-
raise qtrio.InternalError(
131-
"You should not be here but you are running neither PyQt5 nor PySide2.",
132-
)
120+
else:
121+
_reenter_event_type = QtCore.QEvent.Type(event_hint)
133122

134123

135124
async def wait_signal(signal: "QtCore.SignalInstance") -> typing.Tuple[object, ...]:
@@ -541,17 +530,13 @@ def maybe_build_application() -> "QtGui.QGuiApplication":
541530
application: QtCore.QCoreApplication
542531

543532
# TODO: https://bugreports.qt.io/browse/PYSIDE-1467
544-
if qts.is_pyqt_5_wrapper:
545-
maybe_application = QtWidgets.QApplication.instance()
546-
elif qts.is_pyside_5_wrapper:
533+
if qts.is_pyside_5_wrapper:
547534
maybe_application = typing.cast(
548535
typing.Optional["QtCore.QCoreApplication"],
549536
QtWidgets.QApplication.instance(),
550537
)
551-
else: # pragma: no cover
552-
raise qtrio.InternalError(
553-
"You should not be here but you are running neither PyQt5 nor PySide2.",
554-
)
538+
else:
539+
maybe_application = QtWidgets.QApplication.instance()
555540

556541
if maybe_application is None:
557542
application = QtWidgets.QApplication(sys.argv[1:])
@@ -660,7 +645,7 @@ def run(
660645
self.application.aboutToQuit.connect(_early_quit_warning)
661646

662647
if execute_application:
663-
return_code = self.application.exec_()
648+
return_code = qts.util.exec(self.application)
664649

665650
self.outcomes = attr.evolve(
666651
self.outcomes,

qtrio/_qt.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,10 @@ def connection(
129129
finally:
130130
expected_exception: typing.Type[Exception]
131131

132-
if qts.is_pyside_5_wrapper or qts.is_pyside_6_wrapper:
132+
if qts.is_pyside_5_wrapper:
133133
expected_exception = RuntimeError
134+
elif qts.is_pyside_6_wrapper:
135+
expected_exception = SystemError
134136
else:
135137
expected_exception = TypeError
136138

qtrio/_tests/helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
@pytest.fixture(name="qtrio_preshow_workaround", scope="session", autouse=True)
77
def qtrio_preshow_workaround_fixture(qapp):
88
dialog = QtWidgets.QMessageBox(
9-
QtWidgets.QMessageBox.Information,
9+
QtWidgets.QMessageBox.Icon.Information,
1010
"",
1111
"",
12-
QtWidgets.QMessageBox.Ok,
12+
QtWidgets.QMessageBox.StandardButton.Ok,
1313
)
1414

1515
dialog.show()

qtrio/_tests/test_core.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
import outcome
88
import pytest
9+
import qts
910
from qts import QtCore
11+
from qts import QtWidgets
1012
import qtrio
1113
import qtrio._core
1214
import trio
@@ -82,6 +84,7 @@ def test():
8284
sys.platform == "win32"
8385
and sys.version_info >= (3, 11)
8486
and os.environ.get("CI") == "true"
87+
and not qts.is_pyside_6_wrapper
8588
),
8689
reason="the stderr fails to be captured in PyCharm and GitHub Actions",
8790
)
@@ -106,10 +109,18 @@ def test_reenter_event_writes_to_stderr_for_exception(capsys, testdir):
106109
# TODO: diagnostics to be removed
107110
print(repr(result.stderr.str()))
108111
print(result.stderr.str())
112+
if qts.is_pyside_6_wrapper:
113+
internal_error_line = (
114+
r"^qtrio\._exceptions\.InternalError: Error calling Python override of"
115+
r" QObject::event\(\): Exception while handling a reenter event$"
116+
)
117+
else:
118+
internal_error_line = r"^qtrio\._exceptions\.InternalError: Exception while handling a reenter event$"
119+
109120
result.stderr.re_match_lines(
110121
lines2=[
111122
r"^TypeError: 'int' object is not callable$",
112-
r"^qtrio\._exceptions\.InternalError: Exception while handling a reenter event$",
123+
internal_error_line,
113124
],
114125
)
115126

@@ -408,7 +419,7 @@ def test():
408419
pass
409420
410421
with pytest.raises(qtrio.EventTypeRegistrationFailedError):
411-
qtrio.register_requested_event_type(QtCore.QEvent.User)
422+
qtrio.register_requested_event_type(QtCore.QEvent.Type.User)
412423
"""
413424
testdir.makepyfile(test_file)
414425

@@ -446,9 +457,9 @@ def test_requesting_available_event_type_succeeds(testdir):
446457
447458
448459
def test():
449-
qtrio.register_requested_event_type(QtCore.QEvent.User)
460+
qtrio.register_requested_event_type(QtCore.QEvent.Type.User)
450461
451-
assert qtrio.registered_event_type() == QtCore.QEvent.User
462+
assert qtrio.registered_event_type() == QtCore.QEvent.Type.User
452463
"""
453464
testdir.makepyfile(test_file)
454465

@@ -686,6 +697,20 @@ async def test():
686697
)
687698

688699

700+
def test_qobject_destroyed_signal_equality(qapp):
701+
"""Verify that an object's signal instances compare equal."""
702+
q_object = QtCore.QObject()
703+
704+
assert q_object.destroyed == q_object.destroyed
705+
706+
707+
def test_qpushbutton_clicked_signal_equality(qapp):
708+
"""Verify that inherited signal instances compare equal."""
709+
button = QtWidgets.QPushButton()
710+
711+
assert button.clicked == button.clicked
712+
713+
689714
def test_emissions_equal():
690715
""":class:`Emission` objects created from the same :class:`QtCore.Signal` instance
691716
and args are equal even if the attributes are different instances.
@@ -701,6 +726,15 @@ class C(QtCore.QObject):
701726
) == qtrio._core.Emission(signal=instance.signal, args=(13,))
702727

703728

729+
def test_emissions_for_buttons():
730+
""":class:`Emission` objects for the same button signal are equal."""
731+
instance = QtWidgets.QPushButton()
732+
733+
assert qtrio._core.Emission(
734+
signal=instance.clicked, args=(13,)
735+
) == qtrio._core.Emission(signal=instance.clicked, args=(13,))
736+
737+
704738
def test_emissions_unequal_by_signal():
705739
""":class:`Emission` objects with the same arguments but different signals are
706740
unequal.
@@ -1169,6 +1203,8 @@ def test_execute_manually(testdir):
11691203
"""Executing manually works."""
11701204

11711205
test_file = r"""
1206+
import qts.util
1207+
11721208
import qtrio
11731209
11741210
@@ -1184,7 +1220,7 @@ async def async_fn():
11841220
11851221
assert not ran
11861222
1187-
runner.application.exec_()
1223+
qts.util.exec(runner.application)
11881224
11891225
assert ran
11901226
"""

qtrio/_tests/test_dialogs.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ async def user(task_status):
5151
task_status.started()
5252

5353
qtbot.keyClicks(dialog.edit_widget, str(test_value))
54-
qtbot.mouseClick(dialog.accept_button, QtCore.Qt.LeftButton)
54+
qtbot.mouseClick(dialog.accept_button, QtCore.Qt.MouseButton.LeftButton)
5555

5656
test_value = 928
5757

@@ -73,7 +73,7 @@ async def user(task_status):
7373
task_status.started()
7474

7575
qtbot.keyClicks(dialog.edit_widget, "abc")
76-
qtbot.mouseClick(dialog.reject_button, QtCore.Qt.LeftButton)
76+
qtbot.mouseClick(dialog.reject_button, QtCore.Qt.MouseButton.LeftButton)
7777

7878
async with trio.open_nursery() as nursery:
7979
await nursery.start(user)
@@ -92,7 +92,7 @@ async def user(task_status):
9292
task_status.started()
9393

9494
qtbot.keyClicks(dialog.edit_widget, "abc")
95-
qtbot.mouseClick(dialog.accept_button, QtCore.Qt.LeftButton)
95+
qtbot.mouseClick(dialog.accept_button, QtCore.Qt.MouseButton.LeftButton)
9696

9797
async with trio.open_nursery() as nursery:
9898
await nursery.start(user)
@@ -229,7 +229,7 @@ async def test_information_message_box(qtbot: pytestqt.qtbot.QtBot) -> None:
229229
dialog = qtrio.dialogs.create_message_box(
230230
title="Information",
231231
text=text,
232-
icon=QtWidgets.QMessageBox.Information,
232+
icon=QtWidgets.QMessageBox.Icon.Information,
233233
)
234234

235235
async def user(task_status):
@@ -255,8 +255,11 @@ async def test_information_message_box_cancel(qtbot: pytestqt.qtbot.QtBot) -> No
255255
dialog = qtrio.dialogs.create_message_box(
256256
title="",
257257
text="",
258-
icon=QtWidgets.QMessageBox.Information,
259-
buttons=QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
258+
icon=QtWidgets.QMessageBox.Icon.Information,
259+
buttons=(
260+
QtWidgets.QMessageBox.StandardButton.Ok
261+
| QtWidgets.QMessageBox.StandardButton.Cancel
262+
),
260263
)
261264

262265
async def user(task_status):

0 commit comments

Comments
 (0)