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