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