Show More
@@ -19,6 +19,7 b' backends = {' | |||
|
19 | 19 | "wx": "WXAgg", |
|
20 | 20 | "qt4": "Qt4Agg", |
|
21 | 21 | "qt5": "Qt5Agg", |
|
22 | "qt6": "QtAgg", | |
|
22 | 23 | "qt": "Qt5Agg", |
|
23 | 24 | "osx": "MacOSX", |
|
24 | 25 | "nbagg": "nbAgg", |
@@ -32,15 +32,42 b' import os' | |||
|
32 | 32 | import sys |
|
33 | 33 | |
|
34 | 34 | from IPython.utils.version import check_version |
|
35 |
from IPython.external.qt_loaders import ( |
|
|
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, | |
|
37 | loaded_api, | |
|
38 | enum_factory, | |
|
39 | # QT6 | |
|
40 | QT_API_PYQT6, | |
|
41 | QT_API_PYSIDE6, | |
|
42 | # QT5 | |
|
43 | QT_API_PYQT5, | |
|
44 | QT_API_PYSIDE2, | |
|
45 | # QT4 | |
|
46 | QT_API_PYQTv1, | |
|
47 | QT_API_PYQT, | |
|
48 | QT_API_PYSIDE, | |
|
49 | # default | |
|
50 | QT_API_PYQT_DEFAULT, | |
|
51 | ) | |
|
52 | ||
|
53 | _qt_apis = ( | |
|
54 | # QT6 | |
|
55 | QT_API_PYQT6, | |
|
56 | QT_API_PYSIDE6, | |
|
57 | # QT5 | |
|
58 | QT_API_PYQT5, | |
|
59 | QT_API_PYSIDE2, | |
|
60 | # QT4 | |
|
61 | QT_API_PYQTv1, | |
|
62 | QT_API_PYQT, | |
|
63 | QT_API_PYSIDE, | |
|
64 | # default | |
|
65 | QT_API_PYQT_DEFAULT, | |
|
66 | ) | |
|
38 | 67 | |
|
39 | _qt_apis = (QT_API_PYSIDE, QT_API_PYSIDE2, QT_API_PYQT, QT_API_PYQT5, QT_API_PYQTv1, | |
|
40 | QT_API_PYQT_DEFAULT) | |
|
41 | 68 | |
|
42 | #Constraints placed on an imported matplotlib | |
|
43 | 69 | def matplotlib_options(mpl): |
|
70 | """Constraints placed on an imported matplotlib.""" | |
|
44 | 71 | if mpl is None: |
|
45 | 72 | return |
|
46 | 73 | backend = mpl.rcParams.get('backend', None) |
@@ -66,9 +93,7 b' def matplotlib_options(mpl):' | |||
|
66 | 93 | mpqt) |
|
67 | 94 | |
|
68 | 95 | def get_options(): |
|
69 | """Return a list of acceptable QT APIs, in decreasing order of | |
|
70 | preference | |
|
71 | """ | |
|
96 | """Return a list of acceptable QT APIs, in decreasing order of preference.""" | |
|
72 | 97 | #already imported Qt somewhere. Use that |
|
73 | 98 | loaded = loaded_api() |
|
74 | 99 | if loaded is not None: |
@@ -83,13 +108,22 b' def get_options():' | |||
|
83 | 108 | qt_api = os.environ.get('QT_API', None) |
|
84 | 109 | if qt_api is None: |
|
85 | 110 | #no ETS variable. Ask mpl, then use default fallback path |
|
86 |
return matplotlib_options(mpl) or [ |
|
|
87 | QT_API_PYQT5, QT_API_PYSIDE2] | |
|
111 | return matplotlib_options(mpl) or [ | |
|
112 | QT_API_PYQT_DEFAULT, | |
|
113 | QT_API_PYQT6, | |
|
114 | QT_API_PYSIDE6, | |
|
115 | QT_API_PYQT5, | |
|
116 | QT_API_PYSIDE2, | |
|
117 | QT_API_PYQT, | |
|
118 | QT_API_PYSIDE, | |
|
119 | ] | |
|
88 | 120 | elif qt_api not in _qt_apis: |
|
89 | 121 | raise RuntimeError("Invalid Qt API %r, valid values are: %r" % |
|
90 | 122 | (qt_api, ', '.join(_qt_apis))) |
|
91 | 123 | else: |
|
92 | 124 | return [qt_api] |
|
93 | 125 | |
|
126 | ||
|
94 | 127 | api_opts = get_options() |
|
95 | 128 | QtCore, QtGui, QtSvg, QT_API = load_qt(api_opts) |
|
129 | enum_helper = enum_factory(QT_API, QtCore) |
@@ -10,26 +10,41 b' be accessed directly from the outside' | |||
|
10 | 10 | """ |
|
11 | 11 | import sys |
|
12 | 12 | import types |
|
13 | from functools import partial | |
|
14 | from importlib import import_module | |
|
13 | from functools import partial, lru_cache | |
|
14 | import operator | |
|
15 | 15 | |
|
16 | 16 | from IPython.utils.version import check_version |
|
17 | 17 | |
|
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 | |
|
20 | 24 | 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' | |
|
24 | 25 | QT_API_PYSIDE2 = 'pyside2' |
|
25 | 26 | |
|
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 | } | |
|
33 | 48 | |
|
34 | 49 | |
|
35 | 50 | class ImportDenier(object): |
@@ -56,6 +71,7 b' class ImportDenier(object):' | |||
|
56 | 71 | already imported an Incompatible QT Binding: %s |
|
57 | 72 | """ % (fullname, loaded_api())) |
|
58 | 73 | |
|
74 | ||
|
59 | 75 | ID = ImportDenier() |
|
60 | 76 | sys.meta_path.insert(0, ID) |
|
61 | 77 | |
@@ -63,23 +79,11 b' sys.meta_path.insert(0, ID)' | |||
|
63 | 79 | def commit_api(api): |
|
64 | 80 | """Commit to a particular API, and trigger ImportErrors on subsequent |
|
65 | 81 | dangerous imports""" |
|
82 | modules = set(api_to_module.values()) | |
|
66 | 83 | |
|
67 | if api == QT_API_PYSIDE2: | |
|
68 | ID.forbid('PySide') | |
|
69 |
ID.forbid( |
|
|
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) | |
|
83 | 87 | |
|
84 | 88 | |
|
85 | 89 | def loaded_api(): |
@@ -90,19 +94,24 b' def loaded_api():' | |||
|
90 | 94 | |
|
91 | 95 | Returns |
|
92 | 96 | ------- |
|
93 |
None, 'pyside2', 'pyside', 'pyqt', 'pyqt5', |
|
|
97 | None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1' | |
|
94 | 98 | """ |
|
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"): | |
|
96 | 108 | if qtapi_version() == 2: |
|
97 | 109 | return QT_API_PYQT |
|
98 | 110 | else: |
|
99 | 111 | return QT_API_PYQTv1 |
|
100 | elif 'PySide.QtCore' in sys.modules: | |
|
112 | elif sys.modules.get("PySide.QtCore"): | |
|
101 | 113 | 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 | ||
|
106 | 115 | return None |
|
107 | 116 | |
|
108 | 117 | |
@@ -122,7 +131,7 b' def has_binding(api):' | |||
|
122 | 131 | from importlib.util import find_spec |
|
123 | 132 | |
|
124 | 133 | 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): | |
|
126 | 135 | # QT5 requires QtWidgets too |
|
127 | 136 | required.append('QtWidgets') |
|
128 | 137 | |
@@ -174,7 +183,7 b' def can_import(api):' | |||
|
174 | 183 | |
|
175 | 184 | current = loaded_api() |
|
176 | 185 | if api == QT_API_PYQT_DEFAULT: |
|
177 |
return current in [QT_API_PYQT |
|
|
186 | return current in [QT_API_PYQT6, None] | |
|
178 | 187 | else: |
|
179 | 188 | return current in [api, None] |
|
180 | 189 | |
@@ -224,7 +233,7 b' def import_pyqt5():' | |||
|
224 | 233 | """ |
|
225 | 234 | |
|
226 | 235 | from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui |
|
227 | ||
|
236 | ||
|
228 | 237 | # Alias PyQt-specific functions for PySide compatibility. |
|
229 | 238 | QtCore.Signal = QtCore.pyqtSignal |
|
230 | 239 | QtCore.Slot = QtCore.pyqtSlot |
@@ -238,6 +247,28 b' def import_pyqt5():' | |||
|
238 | 247 | return QtCore, QtGuiCompat, QtSvg, api |
|
239 | 248 | |
|
240 | 249 | |
|
250 | def import_pyqt6(): | |
|
251 | """ | |
|
252 | Import PyQt6 | |
|
253 | ||
|
254 | ImportErrors rasied within this function are non-recoverable | |
|
255 | """ | |
|
256 | ||
|
257 | from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui | |
|
258 | ||
|
259 | # Alias PyQt-specific functions for PySide compatibility. | |
|
260 | QtCore.Signal = QtCore.pyqtSignal | |
|
261 | QtCore.Slot = QtCore.pyqtSlot | |
|
262 | ||
|
263 | # Join QtGui and QtWidgets for Qt4 compatibility. | |
|
264 | QtGuiCompat = types.ModuleType("QtGuiCompat") | |
|
265 | QtGuiCompat.__dict__.update(QtGui.__dict__) | |
|
266 | QtGuiCompat.__dict__.update(QtWidgets.__dict__) | |
|
267 | ||
|
268 | api = QT_API_PYQT6 | |
|
269 | return QtCore, QtGuiCompat, QtSvg, api | |
|
270 | ||
|
271 | ||
|
241 | 272 | def import_pyside(): |
|
242 | 273 | """ |
|
243 | 274 | Import PySide |
@@ -264,6 +295,23 b' def import_pyside2():' | |||
|
264 | 295 | return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2 |
|
265 | 296 | |
|
266 | 297 | |
|
298 | def import_pyside6(): | |
|
299 | """ | |
|
300 | Import PySide6 | |
|
301 | ||
|
302 | ImportErrors raised within this function are non-recoverable | |
|
303 | """ | |
|
304 | from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport | |
|
305 | ||
|
306 | # Join QtGui and QtWidgets for Qt4 compatibility. | |
|
307 | QtGuiCompat = types.ModuleType("QtGuiCompat") | |
|
308 | QtGuiCompat.__dict__.update(QtGui.__dict__) | |
|
309 | QtGuiCompat.__dict__.update(QtWidgets.__dict__) | |
|
310 | QtGuiCompat.__dict__.update(QtPrintSupport.__dict__) | |
|
311 | ||
|
312 | return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6 | |
|
313 | ||
|
314 | ||
|
267 | 315 | def load_qt(api_options): |
|
268 | 316 | """ |
|
269 | 317 | Attempt to import Qt, given a preference list |
@@ -291,13 +339,19 b' def load_qt(api_options):' | |||
|
291 | 339 | an incompatible library has already been installed) |
|
292 | 340 | """ |
|
293 | 341 | loaders = { |
|
294 | QT_API_PYSIDE2: import_pyside2, | |
|
295 |
|
|
|
296 |
|
|
|
297 | QT_API_PYQT5: import_pyqt5, | |
|
298 |
|
|
|
299 |
|
|
|
300 |
|
|
|
342 | # Qt6 | |
|
343 | QT_API_PYQT6: import_pyqt6, | |
|
344 | QT_API_PYSIDE6: import_pyside6, | |
|
345 | # Qt5 | |
|
346 | QT_API_PYQT5: import_pyqt5, | |
|
347 | QT_API_PYSIDE2: import_pyside2, | |
|
348 | # Qt4 | |
|
349 | QT_API_PYSIDE: import_pyside, | |
|
350 | QT_API_PYQT: import_pyqt4, | |
|
351 | QT_API_PYQTv1: partial(import_pyqt4, version=1), | |
|
352 | # default | |
|
353 | QT_API_PYQT_DEFAULT: import_pyqt6, | |
|
354 | } | |
|
301 | 355 | |
|
302 | 356 | for api in api_options: |
|
303 | 357 | |
@@ -332,3 +386,16 b' def load_qt(api_options):' | |||
|
332 | 386 | has_binding(QT_API_PYSIDE), |
|
333 | 387 | has_binding(QT_API_PYSIDE2), |
|
334 | 388 | api_options)) |
|
389 | ||
|
390 | ||
|
391 | def enum_factory(QT_API, QtCore): | |
|
392 | """Construct an enum helper to account for PyQt5 <-> PyQt6 changes.""" | |
|
393 | ||
|
394 | @lru_cache(None) | |
|
395 | def _enum(name): | |
|
396 | # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6). | |
|
397 | return operator.attrgetter( | |
|
398 | name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0] | |
|
399 | )(sys.modules[QtCore.__package__]) | |
|
400 | ||
|
401 | return _enum |
@@ -7,13 +7,19 b' aliases = {' | |||
|
7 | 7 | } |
|
8 | 8 | |
|
9 | 9 | backends = [ |
|
10 | 'qt', 'qt4', 'qt5', | |
|
11 | 'gtk', 'gtk2', 'gtk3', | |
|
12 |
|
|
|
13 | 'wx', | |
|
14 | 'pyglet', 'glut', | |
|
15 | 'osx', | |
|
16 | 'asyncio' | |
|
10 | "qt", | |
|
11 | "qt4", | |
|
12 | "qt5", | |
|
13 | "qt6", | |
|
14 | "gtk", | |
|
15 | "gtk2", | |
|
16 | "gtk3", | |
|
17 | "tk", | |
|
18 | "wx", | |
|
19 | "pyglet", | |
|
20 | "glut", | |
|
21 | "osx", | |
|
22 | "asyncio", | |
|
17 | 23 | ] |
|
18 | 24 | |
|
19 | 25 | registered = {} |
@@ -22,6 +28,7 b' def register(name, inputhook):' | |||
|
22 | 28 | """Register the function *inputhook* as an event loop integration.""" |
|
23 | 29 | registered[name] = inputhook |
|
24 | 30 | |
|
31 | ||
|
25 | 32 | class UnknownBackend(KeyError): |
|
26 | 33 | def __init__(self, name): |
|
27 | 34 | self.name = name |
@@ -31,6 +38,7 b' class UnknownBackend(KeyError):' | |||
|
31 | 38 | "Supported event loops are: {}").format(self.name, |
|
32 | 39 | ', '.join(backends + sorted(registered))) |
|
33 | 40 | |
|
41 | ||
|
34 | 42 | def get_inputhook_name_and_func(gui): |
|
35 | 43 | if gui in registered: |
|
36 | 44 | return gui, registered[gui] |
@@ -42,9 +50,12 b' def get_inputhook_name_and_func(gui):' | |||
|
42 | 50 | return get_inputhook_name_and_func(aliases[gui]) |
|
43 | 51 | |
|
44 | 52 | gui_mod = gui |
|
45 |
if gui == |
|
|
46 |
os.environ[ |
|
|
47 |
gui_mod = |
|
|
53 | if gui == "qt5": | |
|
54 | os.environ["QT_API"] = "pyqt5" | |
|
55 | gui_mod = "qt" | |
|
56 | elif gui == "qt6": | |
|
57 | os.environ["QT_API"] = "pyqt6" | |
|
58 | gui_mod = "qt" | |
|
48 | 59 | |
|
49 | 60 | mod = importlib.import_module('IPython.terminal.pt_inputhooks.'+gui_mod) |
|
50 | 61 | return gui, mod.inputhook |
@@ -1,6 +1,6 b'' | |||
|
1 | 1 | import sys |
|
2 | 2 | import os |
|
3 | from IPython.external.qt_for_kernel import QtCore, QtGui | |
|
3 | from IPython.external.qt_for_kernel import QtCore, QtGui, enum_helper | |
|
4 | 4 | from IPython import get_ipython |
|
5 | 5 | |
|
6 | 6 | # If we create a QApplication, keep a reference to it so that it doesn't get |
@@ -9,6 +9,11 b' _appref = None' | |||
|
9 | 9 | _already_warned = False |
|
10 | 10 | |
|
11 | 11 | |
|
12 | def _exec(obj): | |
|
13 | # exec on PyQt6, exec_ elsewhere. | |
|
14 | obj.exec() if hasattr(obj, "exec") else obj.exec_() | |
|
15 | ||
|
16 | ||
|
12 | 17 | def _reclaim_excepthook(): |
|
13 | 18 | shell = get_ipython() |
|
14 | 19 | if shell is not None: |
@@ -32,7 +37,16 b' def inputhook(context):' | |||
|
32 | 37 | 'variable. Deactivate Qt5 code.' |
|
33 | 38 | ) |
|
34 | 39 | return |
|
35 | QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling) | |
|
40 | try: | |
|
41 | QtCore.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling) | |
|
42 | except AttributeError: # Only for Qt>=5.6, <6. | |
|
43 | pass | |
|
44 | try: | |
|
45 | QtCore.QApplication.setHighDpiScaleFactorRoundingPolicy( | |
|
46 | QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough | |
|
47 | ) | |
|
48 | except AttributeError: # Only for Qt>=5.14. | |
|
49 | pass | |
|
36 | 50 | _appref = app = QtGui.QApplication([" "]) |
|
37 | 51 | |
|
38 | 52 | # "reclaim" IPython sys.excepthook after event loop starts |
@@ -55,14 +69,15 b' def inputhook(context):' | |||
|
55 | 69 | else: |
|
56 | 70 | # On POSIX platforms, we can use a file descriptor to quit the event |
|
57 | 71 | # loop when there is input ready to read. |
|
58 |
notifier = QtCore.QSocketNotifier( |
|
|
59 | QtCore.QSocketNotifier.Read) | |
|
72 | notifier = QtCore.QSocketNotifier( | |
|
73 | context.fileno(), enum_helper("QtCore.QSocketNotifier.Type").Read | |
|
74 | ) | |
|
60 | 75 | try: |
|
61 | 76 | # connect the callback we care about before we turn it on |
|
62 | 77 | notifier.activated.connect(lambda: event_loop.exit()) |
|
63 | 78 | notifier.setEnabled(True) |
|
64 | 79 | # only start the event loop we are not already flipped |
|
65 | 80 | if not context.input_is_ready(): |
|
66 |
event_loop |
|
|
81 | _exec(event_loop) | |
|
67 | 82 | finally: |
|
68 | 83 | notifier.setEnabled(False) |
General Comments 0
You need to be logged in to leave comments.
Login now