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