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