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