##// END OF EJS Templates
Merge pull request #11613 from flutefreak7/master...
Matthias Bussonnier -
r24951:19b47faf merge
parent child Browse files
Show More
@@ -1,330 +1,329
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
224 223 # Alias PyQt-specific functions for PySide compatibility.
225 224 QtCore.Signal = QtCore.pyqtSignal
226 225 QtCore.Slot = QtCore.pyqtSlot
227 226
228 227 # Join QtGui and QtWidgets for Qt4 compatibility.
229 228 QtGuiCompat = types.ModuleType('QtGuiCompat')
230 229 QtGuiCompat.__dict__.update(QtGui.__dict__)
231 230 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
232 231
233 232 api = QT_API_PYQT5
234 233 return QtCore, QtGuiCompat, QtSvg, api
235 234
236 235
237 236 def import_pyside():
238 237 """
239 238 Import PySide
240 239
241 240 ImportErrors raised within this function are non-recoverable
242 241 """
243 242 from PySide import QtGui, QtCore, QtSvg
244 243 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
245 244
246 245 def import_pyside2():
247 246 """
248 247 Import PySide2
249 248
250 249 ImportErrors raised within this function are non-recoverable
251 250 """
252 251 from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
253 252
254 253 # Join QtGui and QtWidgets for Qt4 compatibility.
255 254 QtGuiCompat = types.ModuleType('QtGuiCompat')
256 255 QtGuiCompat.__dict__.update(QtGui.__dict__)
257 256 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
258 257 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
259 258
260 259 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
261 260
262 261
263 262 def load_qt(api_options):
264 263 """
265 264 Attempt to import Qt, given a preference list
266 265 of permissible bindings
267 266
268 267 It is safe to call this function multiple times.
269 268
270 269 Parameters
271 270 ----------
272 271 api_options: List of strings
273 272 The order of APIs to try. Valid items are 'pyside', 'pyside2',
274 273 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
275 274
276 275 Returns
277 276 -------
278 277
279 278 A tuple of QtCore, QtGui, QtSvg, QT_API
280 279 The first three are the Qt modules. The last is the
281 280 string indicating which module was loaded.
282 281
283 282 Raises
284 283 ------
285 284 ImportError, if it isn't possible to import any requested
286 285 bindings (either becaues they aren't installed, or because
287 286 an incompatible library has already been installed)
288 287 """
289 288 loaders = {
290 289 QT_API_PYSIDE2: import_pyside2,
291 290 QT_API_PYSIDE: import_pyside,
292 291 QT_API_PYQT: import_pyqt4,
293 292 QT_API_PYQT5: import_pyqt5,
294 293 QT_API_PYQTv1: partial(import_pyqt4, version=1),
295 294 QT_API_PYQT_DEFAULT: partial(import_pyqt4, version=None)
296 295 }
297 296
298 297 for api in api_options:
299 298
300 299 if api not in loaders:
301 300 raise RuntimeError(
302 301 "Invalid Qt API %r, valid values are: %s" %
303 302 (api, ", ".join(["%r" % k for k in loaders.keys()])))
304 303
305 304 if not can_import(api):
306 305 continue
307 306
308 307 #cannot safely recover from an ImportError during this
309 308 result = loaders[api]()
310 309 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
311 310 commit_api(api)
312 311 return result
313 312 else:
314 313 raise ImportError("""
315 314 Could not load requested Qt binding. Please ensure that
316 315 PyQt4 >= 4.7, PyQt5, PySide >= 1.0.3 or PySide2 is available,
317 316 and only one is imported per session.
318 317
319 318 Currently-imported Qt library: %r
320 319 PyQt4 available (requires QtCore, QtGui, QtSvg): %s
321 320 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
322 321 PySide >= 1.0.3 installed: %s
323 322 PySide2 installed: %s
324 323 Tried to load: %r
325 324 """ % (loaded_api(),
326 325 has_binding(QT_API_PYQT),
327 326 has_binding(QT_API_PYQT5),
328 327 has_binding(QT_API_PYSIDE),
329 328 has_binding(QT_API_PYSIDE2),
330 329 api_options))
General Comments 0
You need to be logged in to leave comments. Login now