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