##// END OF EJS Templates
Fix use of pyside6 >= 6.7.0
Ian Thomas -
Show More
@@ -1,410 +1,421 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 def get_attrs(module):
306 return {
307 name: getattr(module, name)
308 for name in dir(module)
309 if not name.startswith("_")
310 }
311
305 312 from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
306 313
307 314 # Join QtGui and QtWidgets for Qt4 compatibility.
308 315 QtGuiCompat = types.ModuleType("QtGuiCompat")
309 316 QtGuiCompat.__dict__.update(QtGui.__dict__)
317 if QtCore.__version_info__ < (6, 7):
310 318 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
311 319 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
320 else:
321 QtGuiCompat.__dict__.update(get_attrs(QtWidgets))
322 QtGuiCompat.__dict__.update(get_attrs(QtPrintSupport))
312 323
313 324 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6
314 325
315 326
316 327 def load_qt(api_options):
317 328 """
318 329 Attempt to import Qt, given a preference list
319 330 of permissible bindings
320 331
321 332 It is safe to call this function multiple times.
322 333
323 334 Parameters
324 335 ----------
325 336 api_options : List of strings
326 337 The order of APIs to try. Valid items are 'pyside', 'pyside2',
327 338 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
328 339
329 340 Returns
330 341 -------
331 342 A tuple of QtCore, QtGui, QtSvg, QT_API
332 343 The first three are the Qt modules. The last is the
333 344 string indicating which module was loaded.
334 345
335 346 Raises
336 347 ------
337 348 ImportError, if it isn't possible to import any requested
338 349 bindings (either because they aren't installed, or because
339 350 an incompatible library has already been installed)
340 351 """
341 352 loaders = {
342 353 # Qt6
343 354 QT_API_PYQT6: import_pyqt6,
344 355 QT_API_PYSIDE6: import_pyside6,
345 356 # Qt5
346 357 QT_API_PYQT5: import_pyqt5,
347 358 QT_API_PYSIDE2: import_pyside2,
348 359 # Qt4
349 360 QT_API_PYSIDE: import_pyside,
350 361 QT_API_PYQT: import_pyqt4,
351 362 QT_API_PYQTv1: partial(import_pyqt4, version=1),
352 363 # default
353 364 QT_API_PYQT_DEFAULT: import_pyqt6,
354 365 }
355 366
356 367 for api in api_options:
357 368
358 369 if api not in loaders:
359 370 raise RuntimeError(
360 371 "Invalid Qt API %r, valid values are: %s" %
361 372 (api, ", ".join(["%r" % k for k in loaders.keys()])))
362 373
363 374 if not can_import(api):
364 375 continue
365 376
366 377 #cannot safely recover from an ImportError during this
367 378 result = loaders[api]()
368 379 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
369 380 commit_api(api)
370 381 return result
371 382 else:
372 383 # Clear the environment variable since it doesn't work.
373 384 if "QT_API" in os.environ:
374 385 del os.environ["QT_API"]
375 386
376 387 raise ImportError(
377 388 """
378 389 Could not load requested Qt binding. Please ensure that
379 390 PyQt4 >= 4.7, PyQt5, PyQt6, PySide >= 1.0.3, PySide2, or
380 391 PySide6 is available, and only one is imported per session.
381 392
382 393 Currently-imported Qt library: %r
383 394 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
384 395 PyQt6 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
385 396 PySide2 installed: %s
386 397 PySide6 installed: %s
387 398 Tried to load: %r
388 399 """
389 400 % (
390 401 loaded_api(),
391 402 has_binding(QT_API_PYQT5),
392 403 has_binding(QT_API_PYQT6),
393 404 has_binding(QT_API_PYSIDE2),
394 405 has_binding(QT_API_PYSIDE6),
395 406 api_options,
396 407 )
397 408 )
398 409
399 410
400 411 def enum_factory(QT_API, QtCore):
401 412 """Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
402 413
403 414 @lru_cache(None)
404 415 def _enum(name):
405 416 # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6).
406 417 return operator.attrgetter(
407 418 name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0]
408 419 )(sys.modules[QtCore.__package__])
409 420
410 421 return _enum
General Comments 0
You need to be logged in to leave comments. Login now