1"""
2This module contains factory functions that attempt
3to return Qt submodules from the various python Qt bindings.
4
5It also protects against double-importing Qt with different
6bindings, which is unstable and likely to crash
7
8This is used primarily by qt and qt_for_kernel, and shouldn't
9be accessed directly from the outside
10"""
11import sys
12from functools import partial
13
14from pydev_ipython.version import check_version
15
16# Available APIs.
17QT_API_PYQT = 'pyqt'
18QT_API_PYQTv1 = 'pyqtv1'
19QT_API_PYQT_DEFAULT = 'pyqtdefault' # don't set SIP explicitly
20QT_API_PYSIDE = 'pyside'
21QT_API_PYQT5 = 'pyqt5'
22
23
24class ImportDenier(object):
25    """Import Hook that will guard against bad Qt imports
26    once IPython commits to a specific binding
27    """
28
29    def __init__(self):
30        self.__forbidden = None
31
32    def forbid(self, module_name):
33        sys.modules.pop(module_name, None)
34        self.__forbidden = module_name
35
36    def find_module(self, mod_name, pth):
37        if pth:
38            return
39        if mod_name == self.__forbidden:
40            return self
41
42    def load_module(self, mod_name):
43        raise ImportError("""
44    Importing %s disabled by IPython, which has
45    already imported an Incompatible QT Binding: %s
46    """ % (mod_name, loaded_api()))
47
48ID = ImportDenier()
49sys.meta_path.append(ID)
50
51
52def commit_api(api):
53    """Commit to a particular API, and trigger ImportErrors on subsequent
54       dangerous imports"""
55
56    if api == QT_API_PYSIDE:
57        ID.forbid('PyQt4')
58        ID.forbid('PyQt5')
59    else:
60        ID.forbid('PySide')
61
62
63def loaded_api():
64    """Return which API is loaded, if any
65
66    If this returns anything besides None,
67    importing any other Qt binding is unsafe.
68
69    Returns
70    -------
71    None, 'pyside', 'pyqt', or 'pyqtv1'
72    """
73    if 'PyQt4.QtCore' in sys.modules:
74        if qtapi_version() == 2:
75            return QT_API_PYQT
76        else:
77            return QT_API_PYQTv1
78    elif 'PySide.QtCore' in sys.modules:
79        return QT_API_PYSIDE
80    elif 'PyQt5.QtCore' in sys.modules:
81        return QT_API_PYQT5
82    return None
83
84
85def has_binding(api):
86    """Safely check for PyQt4 or PySide, without importing
87       submodules
88
89       Parameters
90       ----------
91       api : str [ 'pyqtv1' | 'pyqt' | 'pyside' | 'pyqtdefault']
92            Which module to check for
93
94       Returns
95       -------
96       True if the relevant module appears to be importable
97    """
98    # we can't import an incomplete pyside and pyqt4
99    # this will cause a crash in sip (#1431)
100    # check for complete presence before importing
101    module_name = {QT_API_PYSIDE: 'PySide',
102                   QT_API_PYQT: 'PyQt4',
103                   QT_API_PYQTv1: 'PyQt4',
104                   QT_API_PYQT_DEFAULT: 'PyQt4',
105                   QT_API_PYQT5: 'PyQt5',
106                   }
107    module_name = module_name[api]
108
109    import imp
110    try:
111        #importing top level PyQt4/PySide module is ok...
112        mod = __import__(module_name)
113        #...importing submodules is not
114        imp.find_module('QtCore', mod.__path__)
115        imp.find_module('QtGui', mod.__path__)
116        imp.find_module('QtSvg', mod.__path__)
117
118        #we can also safely check PySide version
119        if api == QT_API_PYSIDE:
120            return check_version(mod.__version__, '1.0.3')
121        else:
122            return True
123    except ImportError:
124        return False
125
126
127def qtapi_version():
128    """Return which QString API has been set, if any
129
130    Returns
131    -------
132    The QString API version (1 or 2), or None if not set
133    """
134    try:
135        import sip
136    except ImportError:
137        return
138    try:
139        return sip.getapi('QString')
140    except ValueError:
141        return
142
143
144def can_import(api):
145    """Safely query whether an API is importable, without importing it"""
146    if not has_binding(api):
147        return False
148
149    current = loaded_api()
150    if api == QT_API_PYQT_DEFAULT:
151        return current in [QT_API_PYQT, QT_API_PYQTv1, QT_API_PYQT5, None]
152    else:
153        return current in [api, None]
154
155
156def import_pyqt4(version=2):
157    """
158    Import PyQt4
159
160    Parameters
161    ----------
162    version : 1, 2, or None
163      Which QString/QVariant API to use. Set to None to use the system
164      default
165
166    ImportErrors raised within this function are non-recoverable
167    """
168    # The new-style string API (version=2) automatically
169    # converts QStrings to Unicode Python strings. Also, automatically unpacks
170    # QVariants to their underlying objects.
171    import sip
172
173    if version is not None:
174        sip.setapi('QString', version)
175        sip.setapi('QVariant', version)
176
177    from PyQt4 import QtGui, QtCore, QtSvg
178
179    if not check_version(QtCore.PYQT_VERSION_STR, '4.7'):
180        raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
181                          QtCore.PYQT_VERSION_STR)
182
183    # Alias PyQt-specific functions for PySide compatibility.
184    QtCore.Signal = QtCore.pyqtSignal
185    QtCore.Slot = QtCore.pyqtSlot
186
187    # query for the API version (in case version == None)
188    version = sip.getapi('QString')
189    api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
190    return QtCore, QtGui, QtSvg, api
191
192def import_pyqt5():
193    """
194    Import PyQt5
195
196    ImportErrors raised within this function are non-recoverable
197    """
198    from PyQt5 import QtGui, QtCore, QtSvg
199
200    # Alias PyQt-specific functions for PySide compatibility.
201    QtCore.Signal = QtCore.pyqtSignal
202    QtCore.Slot = QtCore.pyqtSlot
203
204    return QtCore, QtGui, QtSvg, QT_API_PYQT5
205
206
207def import_pyside():
208    """
209    Import PySide
210
211    ImportErrors raised within this function are non-recoverable
212    """
213    from PySide import QtGui, QtCore, QtSvg  # @UnresolvedImport
214    return QtCore, QtGui, QtSvg, QT_API_PYSIDE
215
216
217def load_qt(api_options):
218    """
219    Attempt to import Qt, given a preference list
220    of permissible bindings
221
222    It is safe to call this function multiple times.
223
224    Parameters
225    ----------
226    api_options: List of strings
227        The order of APIs to try. Valid items are 'pyside',
228        'pyqt', and 'pyqtv1'
229
230    Returns
231    -------
232
233    A tuple of QtCore, QtGui, QtSvg, QT_API
234    The first three are the Qt modules. The last is the
235    string indicating which module was loaded.
236
237    Raises
238    ------
239    ImportError, if it isn't possible to import any requested
240    bindings (either becaues they aren't installed, or because
241    an incompatible library has already been installed)
242    """
243    loaders = {QT_API_PYSIDE: import_pyside,
244               QT_API_PYQT: import_pyqt4,
245               QT_API_PYQTv1: partial(import_pyqt4, version=1),
246               QT_API_PYQT_DEFAULT: partial(import_pyqt4, version=None),
247               QT_API_PYQT5: import_pyqt5,
248               }
249
250    for api in api_options:
251
252        if api not in loaders:
253            raise RuntimeError(
254                "Invalid Qt API %r, valid values are: %r, %r, %r, %r" %
255                (api, QT_API_PYSIDE, QT_API_PYQT,
256                 QT_API_PYQTv1, QT_API_PYQT_DEFAULT, QT_API_PYQT5))
257
258        if not can_import(api):
259            continue
260
261        #cannot safely recover from an ImportError during this
262        result = loaders[api]()
263        api = result[-1]  # changed if api = QT_API_PYQT_DEFAULT
264        commit_api(api)
265        return result
266    else:
267        raise ImportError("""
268    Could not load requested Qt binding. Please ensure that
269    PyQt4 >= 4.7 or PySide >= 1.0.3 is available,
270    and only one is imported per session.
271
272    Currently-imported Qt library:   %r
273    PyQt4 installed:                 %s
274    PyQt5 installed:                 %s
275    PySide >= 1.0.3 installed:       %s
276    Tried to load:                   %r
277    """ % (loaded_api(),
278           has_binding(QT_API_PYQT),
279           has_binding(QT_API_PYQT5),
280           has_binding(QT_API_PYSIDE),
281           api_options))
282