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