Skip to content

Commit a5f2548

Browse files
authored
PR: Make QAction.setShortcut and setShortcuts accept many types (#461)
2 parents c046821 + c8d1144 commit a5f2548

File tree

5 files changed

+179
-24
lines changed

5 files changed

+179
-24
lines changed

qtpy/QtGui.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,18 @@
88

99
"""Provides QtGui classes and functions."""
1010

11-
from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6, QtModuleNotInstalledError
12-
from ._utils import getattr_missing_optional_dep, possibly_static_exec
11+
from functools import partialmethod
12+
13+
from packaging.version import parse
14+
15+
from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6
16+
from . import QT_VERSION as _qt_version
17+
from ._utils import (
18+
getattr_missing_optional_dep,
19+
possibly_static_exec,
20+
set_shortcut,
21+
set_shortcuts,
22+
)
1323

1424
_missing_optional_names = {}
1525

@@ -252,3 +262,42 @@ def movePositionPatched(
252262
# Follow similar approach for `QDropEvent` and child classes
253263
QDropEvent.pos = lambda self: self.position().toPoint()
254264
QDropEvent.posF = lambda self: self.position()
265+
266+
267+
# Make `QAction.setShortcut` and `QAction.setShortcuts` compatible with Qt>=6.4
268+
if PYQT5 or PYSIDE2 or parse(_qt_version) < parse("6.4"):
269+
270+
class _QAction(QAction):
271+
old_set_shortcut = QAction.setShortcut
272+
old_set_shortcuts = QAction.setShortcuts
273+
274+
def setShortcut(self, shortcut):
275+
return set_shortcut(
276+
self,
277+
shortcut,
278+
old_set_shortcut=_QAction.old_set_shortcut,
279+
)
280+
281+
def setShortcuts(self, shortcuts):
282+
return set_shortcuts(
283+
self,
284+
shortcuts,
285+
old_set_shortcuts=_QAction.old_set_shortcuts,
286+
)
287+
288+
_action_set_shortcut = partialmethod(
289+
set_shortcut,
290+
old_set_shortcut=QAction.setShortcut,
291+
)
292+
_action_set_shortcuts = partialmethod(
293+
set_shortcuts,
294+
old_set_shortcuts=QAction.setShortcuts,
295+
)
296+
QAction.setShortcut = _action_set_shortcut
297+
QAction.setShortcuts = _action_set_shortcuts
298+
# Despite the two previous lines!
299+
if (
300+
QAction.setShortcut is not _action_set_shortcut
301+
or QAction.setShortcuts is not _action_set_shortcuts
302+
):
303+
QAction = _QAction

qtpy/QtWidgets.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,15 @@ def __getattr__(name):
3737
elif PYQT6:
3838
from PyQt6 import QtWidgets
3939
from PyQt6.QtGui import (
40-
QAction,
4140
QActionGroup,
4241
QFileSystemModel,
4342
QShortcut,
4443
QUndoCommand,
4544
)
4645
from PyQt6.QtWidgets import *
4746

47+
from qtpy.QtGui import QAction # See spyder-ide/qtpy#461
48+
4849
# Attempt to import QOpenGLWidget, but if that fails,
4950
# don't raise an exception until the name is explicitly accessed.
5051
# See https://github.com/spyder-ide/qtpy/pull/387/
@@ -110,9 +111,11 @@ def __getattr__(name):
110111
elif PYSIDE2:
111112
from PySide2.QtWidgets import *
112113
elif PYSIDE6:
113-
from PySide6.QtGui import QAction, QActionGroup, QShortcut, QUndoCommand
114+
from PySide6.QtGui import QActionGroup, QShortcut, QUndoCommand
114115
from PySide6.QtWidgets import *
115116

117+
from qtpy.QtGui import QAction # See spyder-ide/qtpy#461
118+
116119
# Attempt to import QOpenGLWidget, but if that fails,
117120
# don't raise an exception until the name is explicitly accessed.
118121
# See https://github.com/spyder-ide/qtpy/pull/387/
@@ -208,10 +211,43 @@ def __getattr__(name):
208211
"directory",
209212
)
210213

211-
# Make `addAction` compatible with Qt6 >= 6.3
212-
if PYQT5 or PYSIDE2 or parse(_qt_version) < parse("6.3"):
213-
QMenu.addAction = partialmethod(add_action, old_add_action=QMenu.addAction)
214-
QToolBar.addAction = partialmethod(
214+
# Make `addAction` compatible with Qt6 >= 6.4
215+
if PYQT5 or PYSIDE2 or parse(_qt_version) < parse("6.4"):
216+
217+
class _QMenu(QMenu):
218+
old_add_action = QMenu.addAction
219+
220+
def addAction(self, *args):
221+
return add_action(
222+
self,
223+
*args,
224+
old_add_action=_QMenu.old_add_action,
225+
)
226+
227+
_menu_add_action = partialmethod(
228+
add_action,
229+
old_add_action=QMenu.addAction,
230+
)
231+
QMenu.addAction = _menu_add_action
232+
# Despite the previous line!
233+
if QMenu.addAction is not _menu_add_action:
234+
QMenu = _QMenu
235+
236+
class _QToolBar(QToolBar):
237+
old_add_action = QToolBar.addAction
238+
239+
def addAction(self, *args):
240+
return add_action(
241+
self,
242+
*args,
243+
old_add_action=_QToolBar.old_add_action,
244+
)
245+
246+
_toolbar_add_action = partialmethod(
215247
add_action,
216248
old_add_action=QToolBar.addAction,
217249
)
250+
QToolBar.addAction = _toolbar_add_action
251+
# Despite the previous line!
252+
if QToolBar.addAction is not _toolbar_add_action:
253+
QToolBar = _QToolBar

qtpy/_utils.py

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,24 +70,57 @@ def possibly_static_exec_(cls, *args, **kwargs):
7070
return cls.exec_(*args, **kwargs)
7171

7272

73+
def set_shortcut(self, shortcut, old_set_shortcut):
74+
"""Ensure that the type of `shortcut` is compatible to `QAction.setShortcut`."""
75+
from qtpy.QtCore import Qt
76+
from qtpy.QtGui import QKeySequence
77+
78+
if isinstance(shortcut, (QKeySequence.StandardKey, Qt.Key, int)):
79+
shortcut = QKeySequence(shortcut)
80+
old_set_shortcut(self, shortcut)
81+
82+
83+
def set_shortcuts(self, shortcuts, old_set_shortcuts):
84+
"""Ensure that the type of `shortcuts` is compatible to `QAction.setShortcuts`."""
85+
from qtpy.QtCore import Qt
86+
from qtpy.QtGui import QKeySequence
87+
88+
if isinstance(
89+
shortcuts,
90+
(QKeySequence, QKeySequence.StandardKey, Qt.Key, int, str),
91+
):
92+
shortcuts = (shortcuts,)
93+
94+
shortcuts = tuple(
95+
(
96+
QKeySequence(shortcut)
97+
if isinstance(shortcut, (QKeySequence.StandardKey, Qt.Key, int))
98+
else shortcut
99+
)
100+
for shortcut in shortcuts
101+
)
102+
old_set_shortcuts(self, shortcuts)
103+
104+
73105
def add_action(self, *args, old_add_action):
74106
"""Re-order arguments of `addAction` to backport compatibility with Qt>=6.3."""
75-
from qtpy.QtCore import QObject
107+
from qtpy.QtCore import QObject, Qt
76108
from qtpy.QtGui import QIcon, QKeySequence
77109

78110
action: QAction
79111
icon: QIcon
80112
text: str
81-
shortcut: QKeySequence | QKeySequence.StandardKey | str | int
113+
shortcut: QKeySequence | QKeySequence.StandardKey | Qt.Key | str | int
82114
receiver: QObject
83115
member: bytes
116+
84117
if all(
85118
isinstance(arg, t)
86119
for arg, t in zip(
87120
args,
88121
[
89122
str,
90-
(QKeySequence, QKeySequence.StandardKey, str, int),
123+
(QKeySequence, QKeySequence.StandardKey, Qt.Key, str, int),
91124
QObject,
92125
bytes,
93126
],
@@ -105,16 +138,15 @@ def add_action(self, *args, old_add_action):
105138
text, shortcut, receiver, member = args
106139
action = old_add_action(self, text, receiver, member, shortcut)
107140
else:
108-
return old_add_action(self, *args)
109-
return action
110-
if all(
141+
action = old_add_action(self, *args)
142+
elif all(
111143
isinstance(arg, t)
112144
for arg, t in zip(
113145
args,
114146
[
115147
QIcon,
116148
str,
117-
(QKeySequence, QKeySequence.StandardKey, str, int),
149+
(QKeySequence, QKeySequence.StandardKey, Qt.Key, str, int),
118150
QObject,
119151
bytes,
120152
],
@@ -123,11 +155,11 @@ def add_action(self, *args, old_add_action):
123155
if len(args) == 3:
124156
icon, text, shortcut = args
125157
action = old_add_action(self, icon, text)
126-
action.setShortcut(QKeySequence(shortcut))
158+
action.setShortcut(shortcut)
127159
elif len(args) == 4:
128160
icon, text, shortcut, receiver = args
129161
action = old_add_action(self, icon, text, receiver)
130-
action.setShortcut(QKeySequence(shortcut))
162+
action.setShortcut(shortcut)
131163
elif len(args) == 5:
132164
icon, text, shortcut, receiver, member = args
133165
action = old_add_action(
@@ -136,12 +168,14 @@ def add_action(self, *args, old_add_action):
136168
text,
137169
receiver,
138170
member,
139-
QKeySequence(shortcut),
171+
shortcut,
140172
)
141173
else:
142-
return old_add_action(self, *args)
143-
return action
144-
return old_add_action(self, *args)
174+
action = old_add_action(self, *args)
175+
else:
176+
action = old_add_action(self, *args)
177+
178+
return action
145179

146180

147181
def static_method_kwargs_wrapper(func, from_kwarg_name, to_kwarg_name):

qtpy/tests/test_qtgui.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
import sys
44

55
import pytest
6+
from packaging.version import parse
67

78
from qtpy import (
89
PYQT5,
910
PYQT_VERSION,
1011
PYSIDE2,
1112
PYSIDE6,
13+
QT_VERSION,
1214
QtCore,
1315
QtGui,
1416
QtWidgets,
@@ -177,6 +179,40 @@ def test_qtextcursor_moveposition():
177179
assert cursor.selectedText() == "foo bar baz"
178180

179181

182+
@pytest.mark.skipif(
183+
sys.platform == "darwin" and sys.version_info[:2] == (3, 7),
184+
reason="Stalls on macOS CI with Python 3.7",
185+
)
186+
def test_QAction_functions(qtbot):
187+
"""Test `QtGui.QAction.setShortcut` compatibility with Qt6 types."""
188+
action = QtGui.QAction("QtPy", None)
189+
action.setShortcut(QtGui.QKeySequence.UnknownKey)
190+
action.setShortcuts([QtGui.QKeySequence.UnknownKey])
191+
action.setShortcuts(QtGui.QKeySequence.UnknownKey)
192+
action.setShortcut(QtCore.Qt.Key_F1)
193+
action.setShortcuts([QtCore.Qt.Key_F1])
194+
# The following line fails even for Qt6 == 6.6.
195+
# Don't test the function with a single `QtCore.Qt.Key` argument.
196+
# See the following test.
197+
# action.setShortcuts(QtCore.Qt.Key_F1)
198+
199+
200+
@pytest.mark.skipif(
201+
parse(QT_VERSION) < parse("6.5.0"),
202+
reason="Qt6 >= 6.5 specific test",
203+
)
204+
@pytest.mark.skipif(
205+
sys.platform == "darwin" and sys.version_info[:2] == (3, 7),
206+
reason="Stalls on macOS CI with Python 3.7",
207+
)
208+
@pytest.mark.xfail(strict=True)
209+
def test_QAction_functions_fail(qtbot):
210+
"""Test `QtGui.QAction.setShortcuts` compatibility with `QtCore.Qt.Key` type."""
211+
action = QtGui.QAction("QtPy", None)
212+
# The following line is wrong even for Qt6 == 6.6.
213+
action.setShortcuts(QtCore.Qt.Key_F1)
214+
215+
180216
def test_opengl_imports():
181217
"""
182218
Test for presence of QOpenGL* classes.

qtpy/tests/test_qtwidgets.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,10 @@ def test_QMenu_functions(qtbot):
117117
window = QtWidgets.QMainWindow()
118118
menu = QtWidgets.QMenu(window)
119119
menu.addAction("QtPy")
120-
menu.addAction("QtPy with a shortcut", QtGui.QKeySequence.UnknownKey)
120+
menu.addAction("QtPy with a Qt.Key shortcut", QtCore.Qt.Key_F1)
121121
menu.addAction(
122122
QtGui.QIcon(),
123-
"QtPy with an icon and a shortcut",
123+
"QtPy with an icon and a QKeySequence shortcut",
124124
QtGui.QKeySequence.UnknownKey,
125125
)
126126
window.show()
@@ -148,7 +148,7 @@ def test_QMenu_functions(qtbot):
148148
def test_QToolBar_functions(qtbot):
149149
"""Test `QtWidgets.QToolBar.addAction` compatibility with Qt6 arguments' order."""
150150
toolbar = QtWidgets.QToolBar()
151-
toolbar.addAction("QtPy with a shortcut", QtGui.QKeySequence.UnknownKey)
151+
toolbar.addAction("QtPy with a shortcut", QtCore.Qt.Key_F1)
152152
toolbar.addAction(
153153
QtGui.QIcon(),
154154
"QtPy with an icon and a shortcut",

0 commit comments

Comments
 (0)