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