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