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