##// END OF EJS Templates
Refactor qt import logic. Fixes #2955
Chris Beaumont -
Show More
@@ -0,0 +1,238 b''
1 """
2 This module contains factory functions that attempt
3 to return Qt submodules from the various python Qt bindings.
4
5 It also protects against double-importing Qt with different
6 bindings, which is unstable and likely to crash
7
8 This is used primarily by qt and qt_for_kernel, and shouldn't
9 be accessed directly from the outside
10 """
11 import sys
12 from functools import partial
13
14 from IPython.utils.version import check_version
15
16 # Available APIs.
17 QT_API_PYQT = 'pyqt'
18 QT_API_PYQTv1 = 'pyqtv1'
19 QT_API_PYSIDE = 'pyside'
20
21
22 class ImportDenier(object):
23 """Import Hook that will guard against bad Qt imports
24 once IPython commits to a specific binding
25 """
26 __forbidden = set()
27
28 def __init__(self):
29 self.__forbidden = None
30
31 def forbid(self, module_name):
32 sys.modules.pop(module_name, None)
33 self.__forbidden = module_name
34
35 def find_module(self, mod_name, pth):
36 if pth:
37 return
38 if mod_name == self.__forbidden:
39 return self
40
41 def load_module(self, mod_name):
42 raise ImportError("""
43 Importing %s disabled by IPython, which has
44 already imported an Incompatible QT Binding: %s
45 """ % (mod_name, loaded_api()))
46
47 ID = ImportDenier()
48 sys.meta_path.append(ID)
49
50
51 def commit_api(api):
52 """Commit to a particular API, and trigger ImportErrors on subsequent
53 dangerous imports"""
54
55 if api == QT_API_PYSIDE:
56 ID.forbid('PyQt4')
57 else:
58 ID.forbid('PySide')
59
60
61 def loaded_api():
62 """Return which API is loaded, if any
63
64 If this returns anything besides None,
65 importing any other Qt binding is unsafe.
66
67 Returns
68 -------
69 None, 'pyside', 'pyqt', or 'pyqtv1'
70 """
71 if 'PyQt4.QtCore' in sys.modules:
72 if qtapi_version() == 2:
73 return QT_API_PYQT
74 else:
75 return QT_API_PYQTv1
76 elif 'PySide.QtCore' in sys.modules:
77 return QT_API_PYSIDE
78 return None
79
80
81 def has_binding(api):
82 """Safely check for PyQt4 or PySide, without importing
83 submodules
84
85 Parameters
86 ----------
87 api : str [ 'pyqtv1' | 'pyqt' | 'pyside']
88 Which module to check for
89
90 Returns
91 -------
92 True if the relevant module appears to be importable
93 """
94 # we can't import an incomplete pyside and pyqt4
95 # this will cause a crash in sip (#1431)
96 # check for complete presence before importing
97 module_name = {QT_API_PYSIDE: 'PySide',
98 QT_API_PYQT: 'PyQt4',
99 QT_API_PYQTv1: 'PyQt4'}
100 module_name = module_name[api]
101
102 import imp
103 try:
104 #importing top level PyQt4/PySide module is ok...
105 mod = __import__(module_name)
106 #...importing submodules is not
107 imp.find_module('QtCore', mod.__path__)
108 imp.find_module('QtGui', mod.__path__)
109 imp.find_module('QtSvg', mod.__path__)
110
111 #we can also safely check PySide version
112 if api == QT_API_PYSIDE:
113 return check_version(mod.__version__, '1.0.3')
114 else:
115 return True
116 except ImportError:
117 return False
118
119
120 def qtapi_version():
121 """Return which QString API has been set, if any
122
123 Returns
124 -------
125 The QString API version (1 or 2), or None if not set
126 """
127 try:
128 import sip
129 except ImportError:
130 return
131 try:
132 return sip.getapi('QString')
133 except ValueError:
134 return
135
136
137 def can_import(api):
138 """Safely query whether an API is importable, without importing it"""
139 current = loaded_api()
140 return has_binding(api) and current in [api, None]
141
142
143 def import_pyqt4(version=2):
144 """
145 Import PyQt4
146
147 ImportErrors rasied within this function are non-recoverable
148 """
149 # The new-style string API (version=2) automatically
150 # converts QStrings to Unicode Python strings. Also, automatically unpacks
151 # QVariants to their underlying objects.
152 import sip
153 sip.setapi('QString', version)
154 sip.setapi('QVariant', version)
155
156 from PyQt4 import QtGui, QtCore, QtSvg
157
158 if not check_version(QtCore.PYQT_VERSION_STR, '4.7'):
159 raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
160 QtCore.PYQT_VERSION_STR)
161
162 # Alias PyQt-specific functions for PySide compatibility.
163 QtCore.Signal = QtCore.pyqtSignal
164 QtCore.Slot = QtCore.pyqtSlot
165
166 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
167 return QtCore, QtGui, QtSvg, api
168
169
170 def import_pyside():
171 """
172 Import PySide
173
174 ImportErrors raised within this function are non-recoverable
175 """
176 from PySide import QtGui, QtCore, QtSvg
177 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
178
179
180 def load_qt(api_options):
181 """
182 Attempt to import Qt, given a preference list
183 of permissible bindings
184
185 It is safe to call this function multiple times.
186
187 Parameters
188 ----------
189 api_options: List of strings
190 The order of APIs to try. Valid items are 'pyside',
191 'pyqt', and 'pyqtv1'
192
193 Returns
194 -------
195
196 A tuple of QtCore, QtGui, QtSvg, QT_API
197 The first three are the Qt modules. The last is the
198 string indicating which module was loaded.
199
200 Raises
201 ------
202 ImportError, if it isn't possible to import any requested
203 bindings (either becaues they aren't installed, or because
204 an incompatible library has already been installed)
205 """
206 loaders = {QT_API_PYSIDE: import_pyside,
207 QT_API_PYQT: import_pyqt4,
208 QT_API_PYQTv1: partial(import_pyqt4, version=1)
209 }
210
211 for api in api_options:
212
213 if api not in loaders:
214 raise RuntimeError(
215 "Invalid Qt API %r, valid values are: %r, %r, %r" %
216 (api, QT_API_PYSIDE, QT_API_PYQT, QT_API_PYQTv1))
217
218 if not can_import(api):
219 continue
220
221 #cannot safely recover from an ImportError during this
222 result = loaders[api]()
223 commit_api(api)
224 return result
225 else:
226 raise ImportError("""
227 Could not load requested Qt binding. Please ensure that
228 PyQt4 >= 4.7 or PySide >= 1.0.3 is available,
229 and only one is imported per session.
230
231 Currently-imported Qt library: %r
232 PyQt4 installed: %s
233 PySide >= 1.0.3 installed: %s
234 Tried to load: %r
235 """ % (loaded_api(),
236 has_binding(QT_API_PYQT),
237 has_binding(QT_API_PYSIDE),
238 api_options))
@@ -7,75 +7,17 b' Do not use this if you need PyQt with the old QString/QVariant API.'
7 """
7 """
8
8
9 import os
9 import os
10 from IPython.utils.version import check_version
11 # Available APIs.
12 QT_API_PYQT = 'pyqt'
13 QT_API_PYSIDE = 'pyside'
14
10
15 def prepare_pyqt4():
11 from IPython.external.qt_loaders import (load_qt, QT_API_PYSIDE,
16 # For PySide compatibility, use the new-style string API that automatically
12 QT_API_PYQT)
17 # converts QStrings to Unicode Python strings. Also, automatically unpack
18 # QVariants to their underlying objects.
19 import sip
20 sip.setapi('QString', 2)
21 sip.setapi('QVariant', 2)
22
13
23 # Select Qt binding, using the QT_API environment variable if available.
14 QT_API = os.environ.get('QT_API', None)
24 QT_API = os.environ.get('QT_API')
15 if QT_API not in [QT_API_PYSIDE, QT_API_PYQT, None]:
16 raise RuntimeError("Invalid Qt API %r, valid values are: %r, %r" %
17 (QT_API, QT_API_PYSIDE, QT_API_PYQT))
25 if QT_API is None:
18 if QT_API is None:
26 pyside_found = False
19 api_opts = [QT_API_PYSIDE, QT_API_PYQT]
27 try:
28 import PySide
29 if not check_version(PySide.__version__, '1.0.3'):
30 # old PySide, fallback on PyQt
31 raise ImportError
32 # we can't import an incomplete pyside and pyqt4
33 # this will cause a crash in sip (#1431)
34 # check for complete presence before importing
35 import imp
36 imp.find_module("QtCore", PySide.__path__)
37 imp.find_module("QtGui", PySide.__path__)
38 imp.find_module("QtSvg", PySide.__path__)
39 pyside_found = True
40 from PySide import QtCore, QtGui, QtSvg
41 QT_API = QT_API_PYSIDE
42 except ImportError:
43 try:
44 prepare_pyqt4()
45 import PyQt4
46 from PyQt4 import QtCore, QtGui, QtSvg
47 if pyside_found:
48 print "WARNING: PySide installation incomplete and PyQt4 " \
49 "present.\nThis will likely crash, please install " \
50 "PySide completely, remove PySide or PyQt4 or set " \
51 "the QT_API environment variable to pyqt or pyside"
52 if not check_version(QtCore.PYQT_VERSION_STR, '4.7'):
53 # PyQt 4.6 has issues with null strings returning as None
54 raise ImportError
55 QT_API = QT_API_PYQT
56 except ImportError:
57 raise ImportError('Cannot import PySide >= 1.0.3 or PyQt4 >= 4.7')
58
59 elif QT_API == QT_API_PYQT:
60 # Note: This must be called *before* PyQt4 is imported.
61 prepare_pyqt4()
62
63 # Now peform the imports.
64 if QT_API == QT_API_PYQT:
65 from PyQt4 import QtCore, QtGui, QtSvg
66 if not check_version(QtCore.PYQT_VERSION_STR, '4.7'):
67 raise ImportError("IPython requires PyQt4 >= 4.7, found %s"%QtCore.PYQT_VERSION_STR)
68
69 # Alias PyQt-specific functions for PySide compatibility.
70 QtCore.Signal = QtCore.pyqtSignal
71 QtCore.Slot = QtCore.pyqtSlot
72
73 elif QT_API == QT_API_PYSIDE:
74 import PySide
75 if not check_version(PySide.__version__, '1.0.3'):
76 raise ImportError("IPython requires PySide >= 1.0.3, found %s"%PySide.__version__)
77 from PySide import QtCore, QtGui, QtSvg
78
79 else:
20 else:
80 raise RuntimeError('Invalid Qt API %r, valid values are: %r or %r' %
21 api_opts = [QT_API]
81 (QT_API, QT_API_PYQT, QT_API_PYSIDE))
22
23 QtCore, QtGui, QtSvg, QT_API = load_qt(api_opts)
@@ -4,6 +4,9 b' This is the import used for the `gui=qt` or `pylab=qt` initialization.'
4
4
5 Import Priority:
5 Import Priority:
6
6
7 if Qt4 has been imported anywhere else:
8 use that
9
7 if matplotlib has been imported and doesn't support v2 (<= 1.0.1):
10 if matplotlib has been imported and doesn't support v2 (<= 1.0.1):
8 use PyQt4 @v1
11 use PyQt4 @v1
9
12
@@ -33,56 +36,49 b' import sys'
33
36
34 from IPython.utils.warn import warn
37 from IPython.utils.warn import warn
35 from IPython.utils.version import check_version
38 from IPython.utils.version import check_version
39 from IPython.external.qt_loaders import (load_qt, QT_API_PYSIDE,
40 QT_API_PYQT, QT_API_PYQTv1,
41 loaded_api)
36
42
37 matplotlib = sys.modules.get('matplotlib')
43 #Constraints placed on an imported matplotlib
38 if matplotlib and not check_version(matplotlib.__version__, '1.0.2'):
44 def matplotlib_options(mpl):
39 # 1.0.1 doesn't support pyside or v2, so stick with PyQt @v1,
45 if mpl is None:
40 # and ignore everything else
46 return
41 from PyQt4 import QtCore, QtGui
47 mpqt = mpl.rcParams.get('backend.qt4', None)
42 else:
48 if mpqt is None:
43 # ask QT_API ETS variable *first*
49 return None
44 QT_API = os.environ.get('QT_API', None)
50 if mpqt.lower() == 'pyside':
45 if QT_API is None:
51 return [QT_API_PYSIDE]
46 # QT_API not set, ask matplotlib if it was imported (e.g. `pylab=qt`)
52 elif mpqt.lower() == 'pyqt4':
47 if matplotlib:
53 return [QT_API_PYQTv1]
48 mpqt = matplotlib.rcParams.get('backend.qt4', None)
54 raise ImportError("unhandled value for backend.qt4 from matplotlib: %r" %
49 else:
55 mpqt)
50 mpqt = None
56
51 if mpqt is None:
57 def get_options():
52 # matplotlib not imported or had nothing to say.
58 """Return a list of acceptable QT APIs, in decreasing order of
53 try:
59 preference
54 # default to unconfigured PyQt4
60 """
55 from PyQt4 import QtCore, QtGui
61 #already imported Qt somewhere. Use that
56 except ImportError:
62 loaded = loaded_api()
57 # fallback on PySide
63 if loaded is not None:
58 try:
64 return [loaded]
59 from PySide import QtCore, QtGui
65
60 except ImportError:
66 mpl = sys.modules.get('matplotlib', None)
61 raise ImportError('Cannot import PySide or PyQt4')
67
62 elif mpqt.lower() == 'pyqt4':
68 if mpl is not None and check_version(mpl.__version__, '1.0.2'):
63 # import PyQt4 unconfigured
69 #1.0.1 only supports PyQt4 v1
64 from PyQt4 import QtCore, QtGui
70 return [QT_API_PYQTv1]
65 elif mpqt.lower() == 'pyside':
71
66 from PySide import QtCore, QtGui
72 if os.environ.get('QT_API', None) is None:
67 else:
73 #no ETS variable. Ask mpl, then use either
68 raise ImportError("unhandled value for backend.qt4 from matplotlib: %r"%mpqt)
74 return matplotlib_options(mpl) or [QT_API_PYQTv1, QT_API_PYSIDE]
69 else:
75
70 # QT_API specified, use PySide or PyQt+v2 API from external.qt
76 #ETS variable present. Will fallback to external.qt
71 # this means ETS is likely to be used, which requires v2
77 return None
72 try:
78
73 from IPython.external.qt import QtCore, QtGui
79 api_opts = get_options()
74 except ValueError as e:
80 if api_opts is not None:
75 if 'API' in str(e):
81 QtCore, QtGui, QtSvg, QT_API = load_qt(api_opts)
76 # PyQt4 already imported, and APIv2 couldn't be set
77 # Give more meaningful message, and warn instead of raising
78 warn("""
79 Assigning the ETS variable `QT_API=pyqt` implies PyQt's v2 API for
80 QString and QVariant, but PyQt has already been imported
81 with v1 APIs. You should unset QT_API to work with PyQt4
82 in its default mode.
83 """)
84 # allow it to still work
85 from PyQt4 import QtCore, QtGui
86 else:
87 raise
88
82
83 else: # use ETS variable
84 from IPython.external.qt import QtCore, QtGui, QtSvg, QT_API
General Comments 0
You need to be logged in to leave comments. Login now