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