##// END OF EJS Templates
Fix use of pyside6 >= 6.7.0 (#14510)...
M Bussonnier -
r28842:e5d1a069 merge
parent child Browse files
Show More
@@ -1,410 +1,422 b''
1 1 """
2 2 This module contains factory functions that attempt
3 3 to return Qt submodules from the various python Qt bindings.
4 4
5 5 It also protects against double-importing Qt with different
6 6 bindings, which is unstable and likely to crash
7 7
8 8 This is used primarily by qt and qt_for_kernel, and shouldn't
9 9 be accessed directly from the outside
10 10 """
11 11 import importlib.abc
12 12 import sys
13 13 import os
14 14 import types
15 15 from functools import partial, lru_cache
16 16 import operator
17 17
18 18 # ### Available APIs.
19 19 # Qt6
20 20 QT_API_PYQT6 = "pyqt6"
21 21 QT_API_PYSIDE6 = "pyside6"
22 22
23 23 # Qt5
24 24 QT_API_PYQT5 = 'pyqt5'
25 25 QT_API_PYSIDE2 = 'pyside2'
26 26
27 27 # Qt4
28 28 # NOTE: Here for legacy matplotlib compatibility, but not really supported on the IPython side.
29 29 QT_API_PYQT = "pyqt" # Force version 2
30 30 QT_API_PYQTv1 = "pyqtv1" # Force version 2
31 31 QT_API_PYSIDE = "pyside"
32 32
33 33 QT_API_PYQT_DEFAULT = "pyqtdefault" # use system default for version 1 vs. 2
34 34
35 35 api_to_module = {
36 36 # Qt6
37 37 QT_API_PYQT6: "PyQt6",
38 38 QT_API_PYSIDE6: "PySide6",
39 39 # Qt5
40 40 QT_API_PYQT5: "PyQt5",
41 41 QT_API_PYSIDE2: "PySide2",
42 42 # Qt4
43 43 QT_API_PYSIDE: "PySide",
44 44 QT_API_PYQT: "PyQt4",
45 45 QT_API_PYQTv1: "PyQt4",
46 46 # default
47 47 QT_API_PYQT_DEFAULT: "PyQt6",
48 48 }
49 49
50 50
51 51 class ImportDenier(importlib.abc.MetaPathFinder):
52 52 """Import Hook that will guard against bad Qt imports
53 53 once IPython commits to a specific binding
54 54 """
55 55
56 56 def __init__(self):
57 57 self.__forbidden = set()
58 58
59 59 def forbid(self, module_name):
60 60 sys.modules.pop(module_name, None)
61 61 self.__forbidden.add(module_name)
62 62
63 63 def find_spec(self, fullname, path, target=None):
64 64 if path:
65 65 return
66 66 if fullname in self.__forbidden:
67 67 raise ImportError(
68 68 """
69 69 Importing %s disabled by IPython, which has
70 70 already imported an Incompatible QT Binding: %s
71 71 """
72 72 % (fullname, loaded_api())
73 73 )
74 74
75 75
76 76 ID = ImportDenier()
77 77 sys.meta_path.insert(0, ID)
78 78
79 79
80 80 def commit_api(api):
81 81 """Commit to a particular API, and trigger ImportErrors on subsequent
82 82 dangerous imports"""
83 83 modules = set(api_to_module.values())
84 84
85 85 modules.remove(api_to_module[api])
86 86 for mod in modules:
87 87 ID.forbid(mod)
88 88
89 89
90 90 def loaded_api():
91 91 """Return which API is loaded, if any
92 92
93 93 If this returns anything besides None,
94 94 importing any other Qt binding is unsafe.
95 95
96 96 Returns
97 97 -------
98 98 None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1'
99 99 """
100 100 if sys.modules.get("PyQt6.QtCore"):
101 101 return QT_API_PYQT6
102 102 elif sys.modules.get("PySide6.QtCore"):
103 103 return QT_API_PYSIDE6
104 104 elif sys.modules.get("PyQt5.QtCore"):
105 105 return QT_API_PYQT5
106 106 elif sys.modules.get("PySide2.QtCore"):
107 107 return QT_API_PYSIDE2
108 108 elif sys.modules.get("PyQt4.QtCore"):
109 109 if qtapi_version() == 2:
110 110 return QT_API_PYQT
111 111 else:
112 112 return QT_API_PYQTv1
113 113 elif sys.modules.get("PySide.QtCore"):
114 114 return QT_API_PYSIDE
115 115
116 116 return None
117 117
118 118
119 119 def has_binding(api):
120 120 """Safely check for PyQt4/5, PySide or PySide2, without importing submodules
121 121
122 122 Parameters
123 123 ----------
124 124 api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
125 125 Which module to check for
126 126
127 127 Returns
128 128 -------
129 129 True if the relevant module appears to be importable
130 130 """
131 131 module_name = api_to_module[api]
132 132 from importlib.util import find_spec
133 133
134 134 required = ['QtCore', 'QtGui', 'QtSvg']
135 135 if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6):
136 136 # QT5 requires QtWidgets too
137 137 required.append('QtWidgets')
138 138
139 139 for submod in required:
140 140 try:
141 141 spec = find_spec('%s.%s' % (module_name, submod))
142 142 except ImportError:
143 143 # Package (e.g. PyQt5) not found
144 144 return False
145 145 else:
146 146 if spec is None:
147 147 # Submodule (e.g. PyQt5.QtCore) not found
148 148 return False
149 149
150 150 if api == QT_API_PYSIDE:
151 151 # We can also safely check PySide version
152 152 import PySide
153 153
154 154 return PySide.__version_info__ >= (1, 0, 3)
155 155
156 156 return True
157 157
158 158
159 159 def qtapi_version():
160 160 """Return which QString API has been set, if any
161 161
162 162 Returns
163 163 -------
164 164 The QString API version (1 or 2), or None if not set
165 165 """
166 166 try:
167 167 import sip
168 168 except ImportError:
169 169 # as of PyQt5 5.11, sip is no longer available as a top-level
170 170 # module and needs to be imported from the PyQt5 namespace
171 171 try:
172 172 from PyQt5 import sip
173 173 except ImportError:
174 174 return
175 175 try:
176 176 return sip.getapi('QString')
177 177 except ValueError:
178 178 return
179 179
180 180
181 181 def can_import(api):
182 182 """Safely query whether an API is importable, without importing it"""
183 183 if not has_binding(api):
184 184 return False
185 185
186 186 current = loaded_api()
187 187 if api == QT_API_PYQT_DEFAULT:
188 188 return current in [QT_API_PYQT6, None]
189 189 else:
190 190 return current in [api, None]
191 191
192 192
193 193 def import_pyqt4(version=2):
194 194 """
195 195 Import PyQt4
196 196
197 197 Parameters
198 198 ----------
199 199 version : 1, 2, or None
200 200 Which QString/QVariant API to use. Set to None to use the system
201 201 default
202 202 ImportErrors raised within this function are non-recoverable
203 203 """
204 204 # The new-style string API (version=2) automatically
205 205 # converts QStrings to Unicode Python strings. Also, automatically unpacks
206 206 # QVariants to their underlying objects.
207 207 import sip
208 208
209 209 if version is not None:
210 210 sip.setapi('QString', version)
211 211 sip.setapi('QVariant', version)
212 212
213 213 from PyQt4 import QtGui, QtCore, QtSvg
214 214
215 215 if QtCore.PYQT_VERSION < 0x040700:
216 216 raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
217 217 QtCore.PYQT_VERSION_STR)
218 218
219 219 # Alias PyQt-specific functions for PySide compatibility.
220 220 QtCore.Signal = QtCore.pyqtSignal
221 221 QtCore.Slot = QtCore.pyqtSlot
222 222
223 223 # query for the API version (in case version == None)
224 224 version = sip.getapi('QString')
225 225 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
226 226 return QtCore, QtGui, QtSvg, api
227 227
228 228
229 229 def import_pyqt5():
230 230 """
231 231 Import PyQt5
232 232
233 233 ImportErrors raised within this function are non-recoverable
234 234 """
235 235
236 236 from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
237 237
238 238 # Alias PyQt-specific functions for PySide compatibility.
239 239 QtCore.Signal = QtCore.pyqtSignal
240 240 QtCore.Slot = QtCore.pyqtSlot
241 241
242 242 # Join QtGui and QtWidgets for Qt4 compatibility.
243 243 QtGuiCompat = types.ModuleType('QtGuiCompat')
244 244 QtGuiCompat.__dict__.update(QtGui.__dict__)
245 245 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
246 246
247 247 api = QT_API_PYQT5
248 248 return QtCore, QtGuiCompat, QtSvg, api
249 249
250 250
251 251 def import_pyqt6():
252 252 """
253 253 Import PyQt6
254 254
255 255 ImportErrors raised within this function are non-recoverable
256 256 """
257 257
258 258 from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui
259 259
260 260 # Alias PyQt-specific functions for PySide compatibility.
261 261 QtCore.Signal = QtCore.pyqtSignal
262 262 QtCore.Slot = QtCore.pyqtSlot
263 263
264 264 # Join QtGui and QtWidgets for Qt4 compatibility.
265 265 QtGuiCompat = types.ModuleType("QtGuiCompat")
266 266 QtGuiCompat.__dict__.update(QtGui.__dict__)
267 267 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
268 268
269 269 api = QT_API_PYQT6
270 270 return QtCore, QtGuiCompat, QtSvg, api
271 271
272 272
273 273 def import_pyside():
274 274 """
275 275 Import PySide
276 276
277 277 ImportErrors raised within this function are non-recoverable
278 278 """
279 279 from PySide import QtGui, QtCore, QtSvg
280 280 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
281 281
282 282 def import_pyside2():
283 283 """
284 284 Import PySide2
285 285
286 286 ImportErrors raised within this function are non-recoverable
287 287 """
288 288 from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
289 289
290 290 # Join QtGui and QtWidgets for Qt4 compatibility.
291 291 QtGuiCompat = types.ModuleType('QtGuiCompat')
292 292 QtGuiCompat.__dict__.update(QtGui.__dict__)
293 293 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
294 294 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
295 295
296 296 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
297 297
298 298
299 299 def import_pyside6():
300 300 """
301 301 Import PySide6
302 302
303 303 ImportErrors raised within this function are non-recoverable
304 304 """
305
306 def get_attrs(module):
307 return {
308 name: getattr(module, name)
309 for name in dir(module)
310 if not name.startswith("_")
311 }
312
305 313 from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
306 314
307 315 # Join QtGui and QtWidgets for Qt4 compatibility.
308 316 QtGuiCompat = types.ModuleType("QtGuiCompat")
309 317 QtGuiCompat.__dict__.update(QtGui.__dict__)
310 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
311 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
318 if QtCore.__version_info__ < (6, 7):
319 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
320 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
321 else:
322 QtGuiCompat.__dict__.update(get_attrs(QtWidgets))
323 QtGuiCompat.__dict__.update(get_attrs(QtPrintSupport))
312 324
313 325 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6
314 326
315 327
316 328 def load_qt(api_options):
317 329 """
318 330 Attempt to import Qt, given a preference list
319 331 of permissible bindings
320 332
321 333 It is safe to call this function multiple times.
322 334
323 335 Parameters
324 336 ----------
325 337 api_options : List of strings
326 338 The order of APIs to try. Valid items are 'pyside', 'pyside2',
327 339 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
328 340
329 341 Returns
330 342 -------
331 343 A tuple of QtCore, QtGui, QtSvg, QT_API
332 344 The first three are the Qt modules. The last is the
333 345 string indicating which module was loaded.
334 346
335 347 Raises
336 348 ------
337 349 ImportError, if it isn't possible to import any requested
338 350 bindings (either because they aren't installed, or because
339 351 an incompatible library has already been installed)
340 352 """
341 353 loaders = {
342 354 # Qt6
343 355 QT_API_PYQT6: import_pyqt6,
344 356 QT_API_PYSIDE6: import_pyside6,
345 357 # Qt5
346 358 QT_API_PYQT5: import_pyqt5,
347 359 QT_API_PYSIDE2: import_pyside2,
348 360 # Qt4
349 361 QT_API_PYSIDE: import_pyside,
350 362 QT_API_PYQT: import_pyqt4,
351 363 QT_API_PYQTv1: partial(import_pyqt4, version=1),
352 364 # default
353 365 QT_API_PYQT_DEFAULT: import_pyqt6,
354 366 }
355 367
356 368 for api in api_options:
357 369
358 370 if api not in loaders:
359 371 raise RuntimeError(
360 372 "Invalid Qt API %r, valid values are: %s" %
361 373 (api, ", ".join(["%r" % k for k in loaders.keys()])))
362 374
363 375 if not can_import(api):
364 376 continue
365 377
366 378 #cannot safely recover from an ImportError during this
367 379 result = loaders[api]()
368 380 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
369 381 commit_api(api)
370 382 return result
371 383 else:
372 384 # Clear the environment variable since it doesn't work.
373 385 if "QT_API" in os.environ:
374 386 del os.environ["QT_API"]
375 387
376 388 raise ImportError(
377 389 """
378 390 Could not load requested Qt binding. Please ensure that
379 391 PyQt4 >= 4.7, PyQt5, PyQt6, PySide >= 1.0.3, PySide2, or
380 392 PySide6 is available, and only one is imported per session.
381 393
382 394 Currently-imported Qt library: %r
383 395 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
384 396 PyQt6 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
385 397 PySide2 installed: %s
386 398 PySide6 installed: %s
387 399 Tried to load: %r
388 400 """
389 401 % (
390 402 loaded_api(),
391 403 has_binding(QT_API_PYQT5),
392 404 has_binding(QT_API_PYQT6),
393 405 has_binding(QT_API_PYSIDE2),
394 406 has_binding(QT_API_PYSIDE6),
395 407 api_options,
396 408 )
397 409 )
398 410
399 411
400 412 def enum_factory(QT_API, QtCore):
401 413 """Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
402 414
403 415 @lru_cache(None)
404 416 def _enum(name):
405 417 # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6).
406 418 return operator.attrgetter(
407 419 name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0]
408 420 )(sys.modules[QtCore.__package__])
409 421
410 422 return _enum
General Comments 0
You need to be logged in to leave comments. Login now