##// END OF EJS Templates
Fixed PyQt 5.11 backwards incompatibility by moving import sip after other imports
bnables -
Show More
@@ -1,330 +1,333
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.insert(0, 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 elif api == QT_API_PYSIDE:
71 elif 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 Parameters
112 Parameters
113 ----------
113 ----------
114 api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
114 api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
115 Which module to check for
115 Which module to check for
116
116
117 Returns
117 Returns
118 -------
118 -------
119 True if the relevant module appears to be importable
119 True if the relevant module appears to be importable
120 """
120 """
121 module_name = api_to_module[api]
121 module_name = api_to_module[api]
122 from importlib.util import find_spec
122 from importlib.util import find_spec
123
123
124 required = ['QtCore', 'QtGui', 'QtSvg']
124 required = ['QtCore', 'QtGui', 'QtSvg']
125 if api in (QT_API_PYQT5, QT_API_PYSIDE2):
125 if api in (QT_API_PYQT5, QT_API_PYSIDE2):
126 # QT5 requires QtWidgets too
126 # QT5 requires QtWidgets too
127 required.append('QtWidgets')
127 required.append('QtWidgets')
128
128
129 for submod in required:
129 for submod in required:
130 try:
130 try:
131 spec = find_spec('%s.%s' % (module_name, submod))
131 spec = find_spec('%s.%s' % (module_name, submod))
132 except ImportError:
132 except ImportError:
133 # Package (e.g. PyQt5) not found
133 # Package (e.g. PyQt5) not found
134 return False
134 return False
135 else:
135 else:
136 if spec is None:
136 if spec is None:
137 # Submodule (e.g. PyQt5.QtCore) not found
137 # Submodule (e.g. PyQt5.QtCore) not found
138 return False
138 return False
139
139
140 if api == QT_API_PYSIDE:
140 if api == QT_API_PYSIDE:
141 # We can also safely check PySide version
141 # We can also safely check PySide version
142 import PySide
142 import PySide
143 return check_version(PySide.__version__, '1.0.3')
143 return check_version(PySide.__version__, '1.0.3')
144
144
145 return True
145 return True
146
146
147
147
148 def qtapi_version():
148 def qtapi_version():
149 """Return which QString API has been set, if any
149 """Return which QString API has been set, if any
150
150
151 Returns
151 Returns
152 -------
152 -------
153 The QString API version (1 or 2), or None if not set
153 The QString API version (1 or 2), or None if not set
154 """
154 """
155 try:
155 try:
156 import sip
156 import sip
157 except ImportError:
157 except ImportError:
158 return
158 return
159 try:
159 try:
160 return sip.getapi('QString')
160 return sip.getapi('QString')
161 except ValueError:
161 except ValueError:
162 return
162 return
163
163
164
164
165 def can_import(api):
165 def can_import(api):
166 """Safely query whether an API is importable, without importing it"""
166 """Safely query whether an API is importable, without importing it"""
167 if not has_binding(api):
167 if not has_binding(api):
168 return False
168 return False
169
169
170 current = loaded_api()
170 current = loaded_api()
171 if api == QT_API_PYQT_DEFAULT:
171 if api == QT_API_PYQT_DEFAULT:
172 return current in [QT_API_PYQT, QT_API_PYQTv1, None]
172 return current in [QT_API_PYQT, QT_API_PYQTv1, None]
173 else:
173 else:
174 return current in [api, None]
174 return current in [api, None]
175
175
176
176
177 def import_pyqt4(version=2):
177 def import_pyqt4(version=2):
178 """
178 """
179 Import PyQt4
179 Import PyQt4
180
180
181 Parameters
181 Parameters
182 ----------
182 ----------
183 version : 1, 2, or None
183 version : 1, 2, or None
184 Which QString/QVariant API to use. Set to None to use the system
184 Which QString/QVariant API to use. Set to None to use the system
185 default
185 default
186
186
187 ImportErrors rasied within this function are non-recoverable
187 ImportErrors rasied within this function are non-recoverable
188 """
188 """
189 # The new-style string API (version=2) automatically
189 # The new-style string API (version=2) automatically
190 # converts QStrings to Unicode Python strings. Also, automatically unpacks
190 # converts QStrings to Unicode Python strings. Also, automatically unpacks
191 # QVariants to their underlying objects.
191 # QVariants to their underlying objects.
192 import sip
192 import sip
193
193
194 if version is not None:
194 if version is not None:
195 sip.setapi('QString', version)
195 sip.setapi('QString', version)
196 sip.setapi('QVariant', version)
196 sip.setapi('QVariant', version)
197
197
198 from PyQt4 import QtGui, QtCore, QtSvg
198 from PyQt4 import QtGui, QtCore, QtSvg
199
199
200 if not check_version(QtCore.PYQT_VERSION_STR, '4.7'):
200 if not check_version(QtCore.PYQT_VERSION_STR, '4.7'):
201 raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
201 raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
202 QtCore.PYQT_VERSION_STR)
202 QtCore.PYQT_VERSION_STR)
203
203
204 # Alias PyQt-specific functions for PySide compatibility.
204 # Alias PyQt-specific functions for PySide compatibility.
205 QtCore.Signal = QtCore.pyqtSignal
205 QtCore.Signal = QtCore.pyqtSignal
206 QtCore.Slot = QtCore.pyqtSlot
206 QtCore.Slot = QtCore.pyqtSlot
207
207
208 # query for the API version (in case version == None)
208 # query for the API version (in case version == None)
209 version = sip.getapi('QString')
209 version = sip.getapi('QString')
210 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
210 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
211 return QtCore, QtGui, QtSvg, api
211 return QtCore, QtGui, QtSvg, api
212
212
213
213
214 def import_pyqt5():
214 def import_pyqt5():
215 """
215 """
216 Import PyQt5
216 Import PyQt5
217
217
218 ImportErrors rasied within this function are non-recoverable
218 ImportErrors rasied within this function are non-recoverable
219 """
219 """
220 import sip
221
220
222 from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
221 from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
223
222
223 # Note that import sip must follow other PyQt5 imports for PyQt >5.11
224 # http://pyqt.sourceforge.net/Docs/PyQt5/incompatibilities.html#pyqt-v5-11
225 import sip
226
224 # Alias PyQt-specific functions for PySide compatibility.
227 # Alias PyQt-specific functions for PySide compatibility.
225 QtCore.Signal = QtCore.pyqtSignal
228 QtCore.Signal = QtCore.pyqtSignal
226 QtCore.Slot = QtCore.pyqtSlot
229 QtCore.Slot = QtCore.pyqtSlot
227
230
228 # Join QtGui and QtWidgets for Qt4 compatibility.
231 # Join QtGui and QtWidgets for Qt4 compatibility.
229 QtGuiCompat = types.ModuleType('QtGuiCompat')
232 QtGuiCompat = types.ModuleType('QtGuiCompat')
230 QtGuiCompat.__dict__.update(QtGui.__dict__)
233 QtGuiCompat.__dict__.update(QtGui.__dict__)
231 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
234 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
232
235
233 api = QT_API_PYQT5
236 api = QT_API_PYQT5
234 return QtCore, QtGuiCompat, QtSvg, api
237 return QtCore, QtGuiCompat, QtSvg, api
235
238
236
239
237 def import_pyside():
240 def import_pyside():
238 """
241 """
239 Import PySide
242 Import PySide
240
243
241 ImportErrors raised within this function are non-recoverable
244 ImportErrors raised within this function are non-recoverable
242 """
245 """
243 from PySide import QtGui, QtCore, QtSvg
246 from PySide import QtGui, QtCore, QtSvg
244 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
247 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
245
248
246 def import_pyside2():
249 def import_pyside2():
247 """
250 """
248 Import PySide2
251 Import PySide2
249
252
250 ImportErrors raised within this function are non-recoverable
253 ImportErrors raised within this function are non-recoverable
251 """
254 """
252 from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
255 from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
253
256
254 # Join QtGui and QtWidgets for Qt4 compatibility.
257 # Join QtGui and QtWidgets for Qt4 compatibility.
255 QtGuiCompat = types.ModuleType('QtGuiCompat')
258 QtGuiCompat = types.ModuleType('QtGuiCompat')
256 QtGuiCompat.__dict__.update(QtGui.__dict__)
259 QtGuiCompat.__dict__.update(QtGui.__dict__)
257 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
260 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
258 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
261 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
259
262
260 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
263 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
261
264
262
265
263 def load_qt(api_options):
266 def load_qt(api_options):
264 """
267 """
265 Attempt to import Qt, given a preference list
268 Attempt to import Qt, given a preference list
266 of permissible bindings
269 of permissible bindings
267
270
268 It is safe to call this function multiple times.
271 It is safe to call this function multiple times.
269
272
270 Parameters
273 Parameters
271 ----------
274 ----------
272 api_options: List of strings
275 api_options: List of strings
273 The order of APIs to try. Valid items are 'pyside', 'pyside2',
276 The order of APIs to try. Valid items are 'pyside', 'pyside2',
274 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
277 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
275
278
276 Returns
279 Returns
277 -------
280 -------
278
281
279 A tuple of QtCore, QtGui, QtSvg, QT_API
282 A tuple of QtCore, QtGui, QtSvg, QT_API
280 The first three are the Qt modules. The last is the
283 The first three are the Qt modules. The last is the
281 string indicating which module was loaded.
284 string indicating which module was loaded.
282
285
283 Raises
286 Raises
284 ------
287 ------
285 ImportError, if it isn't possible to import any requested
288 ImportError, if it isn't possible to import any requested
286 bindings (either becaues they aren't installed, or because
289 bindings (either becaues they aren't installed, or because
287 an incompatible library has already been installed)
290 an incompatible library has already been installed)
288 """
291 """
289 loaders = {
292 loaders = {
290 QT_API_PYSIDE2: import_pyside2,
293 QT_API_PYSIDE2: import_pyside2,
291 QT_API_PYSIDE: import_pyside,
294 QT_API_PYSIDE: import_pyside,
292 QT_API_PYQT: import_pyqt4,
295 QT_API_PYQT: import_pyqt4,
293 QT_API_PYQT5: import_pyqt5,
296 QT_API_PYQT5: import_pyqt5,
294 QT_API_PYQTv1: partial(import_pyqt4, version=1),
297 QT_API_PYQTv1: partial(import_pyqt4, version=1),
295 QT_API_PYQT_DEFAULT: partial(import_pyqt4, version=None)
298 QT_API_PYQT_DEFAULT: partial(import_pyqt4, version=None)
296 }
299 }
297
300
298 for api in api_options:
301 for api in api_options:
299
302
300 if api not in loaders:
303 if api not in loaders:
301 raise RuntimeError(
304 raise RuntimeError(
302 "Invalid Qt API %r, valid values are: %s" %
305 "Invalid Qt API %r, valid values are: %s" %
303 (api, ", ".join(["%r" % k for k in loaders.keys()])))
306 (api, ", ".join(["%r" % k for k in loaders.keys()])))
304
307
305 if not can_import(api):
308 if not can_import(api):
306 continue
309 continue
307
310
308 #cannot safely recover from an ImportError during this
311 #cannot safely recover from an ImportError during this
309 result = loaders[api]()
312 result = loaders[api]()
310 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
313 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
311 commit_api(api)
314 commit_api(api)
312 return result
315 return result
313 else:
316 else:
314 raise ImportError("""
317 raise ImportError("""
315 Could not load requested Qt binding. Please ensure that
318 Could not load requested Qt binding. Please ensure that
316 PyQt4 >= 4.7, PyQt5, PySide >= 1.0.3 or PySide2 is available,
319 PyQt4 >= 4.7, PyQt5, PySide >= 1.0.3 or PySide2 is available,
317 and only one is imported per session.
320 and only one is imported per session.
318
321
319 Currently-imported Qt library: %r
322 Currently-imported Qt library: %r
320 PyQt4 available (requires QtCore, QtGui, QtSvg): %s
323 PyQt4 available (requires QtCore, QtGui, QtSvg): %s
321 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
324 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
322 PySide >= 1.0.3 installed: %s
325 PySide >= 1.0.3 installed: %s
323 PySide2 installed: %s
326 PySide2 installed: %s
324 Tried to load: %r
327 Tried to load: %r
325 """ % (loaded_api(),
328 """ % (loaded_api(),
326 has_binding(QT_API_PYQT),
329 has_binding(QT_API_PYQT),
327 has_binding(QT_API_PYQT5),
330 has_binding(QT_API_PYQT5),
328 has_binding(QT_API_PYSIDE),
331 has_binding(QT_API_PYSIDE),
329 has_binding(QT_API_PYSIDE2),
332 has_binding(QT_API_PYSIDE2),
330 api_options))
333 api_options))
General Comments 0
You need to be logged in to leave comments. Login now