Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 459330f

Browse files
committed
ENH: add support for Qt6 input hooks
This is heavily cribbed from Matplotlib's Qt6 support.
1 parent 770024a commit 459330f

5 files changed

Lines changed: 174 additions & 64 deletions

File tree

IPython/core/pylabtools.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"wx": "WXAgg",
2020
"qt4": "Qt4Agg",
2121
"qt5": "Qt5Agg",
22+
"qt6": "QtAgg",
2223
"qt": "Qt5Agg",
2324
"osx": "MacOSX",
2425
"nbagg": "nbAgg",

IPython/external/qt_for_kernel.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,32 @@
3232
import sys
3333

3434
from IPython.utils.version import check_version
35-
from IPython.external.qt_loaders import (load_qt, loaded_api, QT_API_PYSIDE,
36-
QT_API_PYSIDE2, QT_API_PYQT, QT_API_PYQT5,
37-
QT_API_PYQTv1, QT_API_PYQT_DEFAULT)
35+
from IPython.external.qt_loaders import (
36+
load_qt, loaded_api, enum_factory,
37+
# QT6
38+
QT_API_PYQT6, QT_API_PYSIDE6,
39+
# QT5
40+
QT_API_PYQT5, QT_API_PYSIDE2,
41+
# QT4
42+
QT_API_PYQTv1, QT_API_PYQT, QT_API_PYSIDE,
43+
# default
44+
QT_API_PYQT_DEFAULT
45+
)
46+
47+
_qt_apis = (
48+
# QT6
49+
QT_API_PYQT6, QT_API_PYSIDE6,
50+
# QT5
51+
QT_API_PYQT5, QT_API_PYSIDE2,
52+
# QT4
53+
QT_API_PYQTv1, QT_API_PYQT, QT_API_PYSIDE,
54+
# default
55+
QT_API_PYQT_DEFAULT
56+
)
3857

39-
_qt_apis = (QT_API_PYSIDE, QT_API_PYSIDE2, QT_API_PYQT, QT_API_PYQT5, QT_API_PYQTv1,
40-
QT_API_PYQT_DEFAULT)
4158

42-
#Constraints placed on an imported matplotlib
4359
def matplotlib_options(mpl):
60+
"""Constraints placed on an imported matplotlib."""
4461
if mpl is None:
4562
return
4663
backend = mpl.rcParams.get('backend', None)
@@ -66,9 +83,7 @@ def matplotlib_options(mpl):
6683
mpqt)
6784

6885
def get_options():
69-
"""Return a list of acceptable QT APIs, in decreasing order of
70-
preference
71-
"""
86+
"""Return a list of acceptable QT APIs, in decreasing order of preference."""
7287
#already imported Qt somewhere. Use that
7388
loaded = loaded_api()
7489
if loaded is not None:
@@ -83,13 +98,22 @@ def get_options():
8398
qt_api = os.environ.get('QT_API', None)
8499
if qt_api is None:
85100
#no ETS variable. Ask mpl, then use default fallback path
86-
return matplotlib_options(mpl) or [QT_API_PYQT_DEFAULT, QT_API_PYSIDE,
87-
QT_API_PYQT5, QT_API_PYSIDE2]
101+
return matplotlib_options(mpl) or [
102+
QT_API_PYQT_DEFAULT,
103+
QT_API_PYQT6,
104+
QT_API_PYSIDE6,
105+
QT_API_PYQT5,
106+
QT_API_PYSIDE2,
107+
QT_API_PYQT,
108+
QT_API_PYSIDE
109+
]
88110
elif qt_api not in _qt_apis:
89111
raise RuntimeError("Invalid Qt API %r, valid values are: %r" %
90112
(qt_api, ', '.join(_qt_apis)))
91113
else:
92114
return [qt_api]
93115

116+
94117
api_opts = get_options()
95118
QtCore, QtGui, QtSvg, QT_API = load_qt(api_opts)
119+
enum_helper = enum_factory(QT_API, QtCore)

IPython/external/qt_loaders.py

Lines changed: 111 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,41 @@
1010
"""
1111
import sys
1212
import types
13-
from functools import partial
14-
from importlib import import_module
13+
from functools import partial, lru_cache
14+
import operator
1515

1616
from IPython.utils.version import check_version
1717

18-
# Available APIs.
19-
QT_API_PYQT = 'pyqt' # Force version 2
18+
# ### Available APIs.
19+
# Qt6
20+
QT_API_PYQT6 = "pyqt6"
21+
QT_API_PYSIDE6 = "pyside6"
22+
23+
# Qt5
2024
QT_API_PYQT5 = 'pyqt5'
21-
QT_API_PYQTv1 = 'pyqtv1' # Force version 2
22-
QT_API_PYQT_DEFAULT = 'pyqtdefault' # use system default for version 1 vs. 2
23-
QT_API_PYSIDE = 'pyside'
2425
QT_API_PYSIDE2 = 'pyside2'
2526

26-
api_to_module = {QT_API_PYSIDE2: 'PySide2',
27-
QT_API_PYSIDE: 'PySide',
28-
QT_API_PYQT: 'PyQt4',
29-
QT_API_PYQTv1: 'PyQt4',
30-
QT_API_PYQT5: 'PyQt5',
31-
QT_API_PYQT_DEFAULT: 'PyQt4',
32-
}
27+
# Qt4
28+
QT_API_PYQT = 'pyqt' # Force version 2
29+
QT_API_PYQTv1 = 'pyqtv1' # Force version 2
30+
QT_API_PYSIDE = 'pyside'
31+
32+
QT_API_PYQT_DEFAULT = 'pyqtdefault' # use system default for version 1 vs. 2
33+
34+
api_to_module = {
35+
# Qt6
36+
QT_API_PYQT6: "PyQt6",
37+
QT_API_PYSIDE6: "PySide6",
38+
# Qt5
39+
QT_API_PYQT5: 'PyQt5',
40+
QT_API_PYSIDE2: 'PySide2',
41+
# Qt4
42+
QT_API_PYSIDE: 'PySide',
43+
QT_API_PYQT: 'PyQt4',
44+
QT_API_PYQTv1: 'PyQt4',
45+
# default
46+
QT_API_PYQT_DEFAULT: 'PyQt6',
47+
}
3348

3449

3550
class ImportDenier(object):
@@ -56,30 +71,19 @@ def load_module(self, fullname):
5671
already imported an Incompatible QT Binding: %s
5772
""" % (fullname, loaded_api()))
5873

74+
5975
ID = ImportDenier()
6076
sys.meta_path.insert(0, ID)
6177

6278

6379
def commit_api(api):
6480
"""Commit to a particular API, and trigger ImportErrors on subsequent
6581
dangerous imports"""
82+
modules = set(api_to_module.values())
6683

67-
if api == QT_API_PYSIDE2:
68-
ID.forbid('PySide')
69-
ID.forbid('PyQt4')
70-
ID.forbid('PyQt5')
71-
elif api == QT_API_PYSIDE:
72-
ID.forbid('PySide2')
73-
ID.forbid('PyQt4')
74-
ID.forbid('PyQt5')
75-
elif api == QT_API_PYQT5:
76-
ID.forbid('PySide2')
77-
ID.forbid('PySide')
78-
ID.forbid('PyQt4')
79-
else: # There are three other possibilities, all representing PyQt4
80-
ID.forbid('PyQt5')
81-
ID.forbid('PySide2')
82-
ID.forbid('PySide')
84+
modules.remove(api_to_module[api])
85+
for mod in modules:
86+
ID.forbid(mod)
8387

8488

8589
def loaded_api():
@@ -90,19 +94,24 @@ def loaded_api():
9094
9195
Returns
9296
-------
93-
None, 'pyside2', 'pyside', 'pyqt', 'pyqt5', or 'pyqtv1'
97+
None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1'
9498
"""
95-
if 'PyQt4.QtCore' in sys.modules:
99+
if sys.modules.get("PyQt6.QtCore"):
100+
return QT_API_PYQT6
101+
elif sys.modules.get("PySide6.QtCore"):
102+
return QT_API_PYSIDE6
103+
elif sys.modules.get("PyQt5.QtCore"):
104+
return QT_API_PYQT5
105+
elif sys.modules.get("PySide2.QtCore"):
106+
return QT_API_PYSIDE2
107+
elif sys.modules.get('PyQt4.QtCore'):
96108
if qtapi_version() == 2:
97109
return QT_API_PYQT
98110
else:
99111
return QT_API_PYQTv1
100-
elif 'PySide.QtCore' in sys.modules:
112+
elif sys.modules.get('PySide.QtCore'):
101113
return QT_API_PYSIDE
102-
elif 'PySide2.QtCore' in sys.modules:
103-
return QT_API_PYSIDE2
104-
elif 'PyQt5.QtCore' in sys.modules:
105-
return QT_API_PYQT5
114+
106115
return None
107116

108117

@@ -122,7 +131,7 @@ def has_binding(api):
122131
from importlib.util import find_spec
123132

124133
required = ['QtCore', 'QtGui', 'QtSvg']
125-
if api in (QT_API_PYQT5, QT_API_PYSIDE2):
134+
if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6):
126135
# QT5 requires QtWidgets too
127136
required.append('QtWidgets')
128137

@@ -174,7 +183,7 @@ def can_import(api):
174183

175184
current = loaded_api()
176185
if api == QT_API_PYQT_DEFAULT:
177-
return current in [QT_API_PYQT, QT_API_PYQTv1, None]
186+
return current in [QT_API_PYQT6, None]
178187
else:
179188
return current in [api, None]
180189

@@ -224,7 +233,7 @@ def import_pyqt5():
224233
"""
225234

226235
from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
227-
236+
228237
# Alias PyQt-specific functions for PySide compatibility.
229238
QtCore.Signal = QtCore.pyqtSignal
230239
QtCore.Slot = QtCore.pyqtSlot
@@ -237,6 +246,27 @@ def import_pyqt5():
237246
api = QT_API_PYQT5
238247
return QtCore, QtGuiCompat, QtSvg, api
239248

249+
def import_pyqt6():
250+
"""
251+
Import PyQt6
252+
253+
ImportErrors rasied within this function are non-recoverable
254+
"""
255+
256+
from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui
257+
258+
# Alias PyQt-specific functions for PySide compatibility.
259+
QtCore.Signal = QtCore.pyqtSignal
260+
QtCore.Slot = QtCore.pyqtSlot
261+
262+
# Join QtGui and QtWidgets for Qt4 compatibility.
263+
QtGuiCompat = types.ModuleType('QtGuiCompat')
264+
QtGuiCompat.__dict__.update(QtGui.__dict__)
265+
QtGuiCompat.__dict__.update(QtWidgets.__dict__)
266+
267+
api = QT_API_PYQT6
268+
return QtCore, QtGuiCompat, QtSvg, api
269+
240270

241271
def import_pyside():
242272
"""
@@ -263,6 +293,22 @@ def import_pyside2():
263293

264294
return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
265295

296+
def import_pyside6():
297+
"""
298+
Import PySide6
299+
300+
ImportErrors raised within this function are non-recoverable
301+
"""
302+
from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
303+
304+
# Join QtGui and QtWidgets for Qt4 compatibility.
305+
QtGuiCompat = types.ModuleType('QtGuiCompat')
306+
QtGuiCompat.__dict__.update(QtGui.__dict__)
307+
QtGuiCompat.__dict__.update(QtWidgets.__dict__)
308+
QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
309+
310+
return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6
311+
266312

267313
def load_qt(api_options):
268314
"""
@@ -291,13 +337,19 @@ def load_qt(api_options):
291337
an incompatible library has already been installed)
292338
"""
293339
loaders = {
294-
QT_API_PYSIDE2: import_pyside2,
295-
QT_API_PYSIDE: import_pyside,
296-
QT_API_PYQT: import_pyqt4,
297-
QT_API_PYQT5: import_pyqt5,
298-
QT_API_PYQTv1: partial(import_pyqt4, version=1),
299-
QT_API_PYQT_DEFAULT: partial(import_pyqt4, version=None)
300-
}
340+
# Qt6
341+
QT_API_PYQT6: import_pyqt6,
342+
QT_API_PYSIDE6: import_pyside6,
343+
# Qt5
344+
QT_API_PYQT5: import_pyqt5,
345+
QT_API_PYSIDE2: import_pyside2,
346+
# Qt4
347+
QT_API_PYSIDE: import_pyside,
348+
QT_API_PYQT: import_pyqt4,
349+
QT_API_PYQTv1: partial(import_pyqt4, version=1),
350+
# default
351+
QT_API_PYQT_DEFAULT: import_pyqt6,
352+
}
301353

302354
for api in api_options:
303355

@@ -332,3 +384,15 @@ def load_qt(api_options):
332384
has_binding(QT_API_PYSIDE),
333385
has_binding(QT_API_PYSIDE2),
334386
api_options))
387+
388+
389+
def enum_factory(QT_API, QtCore):
390+
"""Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
391+
@lru_cache(None)
392+
def _enum(name):
393+
# foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6).
394+
return operator.attrgetter(
395+
name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0]
396+
)(sys.modules[QtCore.__package__])
397+
398+
return _enum

IPython/terminal/pt_inputhooks/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
}
88

99
backends = [
10-
'qt', 'qt4', 'qt5',
10+
'qt', 'qt4', 'qt5', 'qt6',
1111
'gtk', 'gtk2', 'gtk3',
1212
'tk',
1313
'wx',
@@ -22,6 +22,7 @@ def register(name, inputhook):
2222
"""Register the function *inputhook* as an event loop integration."""
2323
registered[name] = inputhook
2424

25+
2526
class UnknownBackend(KeyError):
2627
def __init__(self, name):
2728
self.name = name
@@ -31,6 +32,7 @@ def __str__(self):
3132
"Supported event loops are: {}").format(self.name,
3233
', '.join(backends + sorted(registered)))
3334

35+
3436
def get_inputhook_name_and_func(gui):
3537
if gui in registered:
3638
return gui, registered[gui]
@@ -45,6 +47,9 @@ def get_inputhook_name_and_func(gui):
4547
if gui == 'qt5':
4648
os.environ['QT_API'] = 'pyqt5'
4749
gui_mod = 'qt'
50+
elif gui == 'qt6':
51+
os.environ['QT_API'] = 'pyqt6'
52+
gui_mod = 'qt'
4853

4954
mod = importlib.import_module('IPython.terminal.pt_inputhooks.'+gui_mod)
5055
return gui, mod.inputhook

0 commit comments

Comments
 (0)