|
1 |
| -""" A Qt API selector that can be used to switch between PyQt and PySide. |
2 | 1 | """
|
| 2 | +Qt binding and backend selector. |
| 3 | +
|
| 4 | +The selection logic is as follows: |
| 5 | +- if any of PyQt5, PySide2, PyQt4 or PySide have already been imported (checked |
| 6 | + in that order), use it; |
| 7 | +- otherwise, if the QT_API environment variable (used by Enthought) is |
| 8 | + set, use it to determine which binding to use (but do not change the |
| 9 | + backend based on it; i.e. if the Qt4Agg backend is requested but QT_API |
| 10 | + is set to "pyqt5", then actually use Qt4 with the binding specified by |
| 11 | + ``rcParams["backend.qt4"]``; |
| 12 | +- otherwise, use whatever the rcParams indicate. |
| 13 | +""" |
| 14 | + |
3 | 15 | from __future__ import (absolute_import, division, print_function,
|
4 | 16 | unicode_literals)
|
5 | 17 |
|
6 | 18 | import six
|
7 | 19 |
|
| 20 | +from distutils.version import LooseVersion |
8 | 21 | import os
|
9 |
| -import logging |
10 | 22 | import sys
|
11 |
| -from matplotlib import rcParams |
12 | 23 |
|
13 |
| -_log = logging.getLogger(__name__) |
14 |
| - |
15 |
| -# Available APIs. |
16 |
| -QT_API_PYQT = 'PyQt4' # API is not set here; Python 2.x default is V 1 |
17 |
| -QT_API_PYQTv2 = 'PyQt4v2' # forced to Version 2 API |
18 |
| -QT_API_PYSIDE = 'PySide' # only supports Version 2 API |
19 |
| -QT_API_PYQT5 = 'PyQt5' # use PyQt5 API; Version 2 with module shim |
20 |
| -QT_API_PYSIDE2 = 'PySide2' # Version 2 API with module shim |
| 24 | +from matplotlib import rcParams |
21 | 25 |
|
22 |
| -ETS = dict(pyqt=(QT_API_PYQTv2, 4), pyside=(QT_API_PYSIDE, 4), |
23 |
| - pyqt5=(QT_API_PYQT5, 5), pyside2=(QT_API_PYSIDE2, 5)) |
24 |
| -# ETS is a dict of env variable to (QT_API, QT_MAJOR_VERSION) |
25 |
| -# If the ETS QT_API environment variable is set, use it, but only |
26 |
| -# if the varible if of the same major QT version. Note that |
27 |
| -# ETS requires the version 2 of PyQt4, which is not the platform |
28 |
| -# default for Python 2.x. |
29 | 26 |
|
| 27 | +QT_API_PYQT = "PyQt4" |
| 28 | +QT_API_PYQTv2 = "PyQt4v2" |
| 29 | +QT_API_PYSIDE = "PySide" |
| 30 | +QT_API_PYQT5 = "PyQt5" |
| 31 | +QT_API_PYSIDE2 = "PySide2" |
30 | 32 | QT_API_ENV = os.environ.get('QT_API')
|
31 |
| - |
32 |
| -if rcParams['backend'] == 'Qt5Agg': |
33 |
| - QT_RC_MAJOR_VERSION = 5 |
34 |
| -elif rcParams['backend'] == 'Qt4Agg': |
35 |
| - QT_RC_MAJOR_VERSION = 4 |
| 33 | +# First, check if anything is already imported. |
| 34 | +if "PyQt5" in sys.modules: |
| 35 | + QT_API = rcParams["backend.qt5"] = QT_API_PYQT5 |
| 36 | +elif "PySide2" in sys.modules: |
| 37 | + QT_API = rcParams["backend.qt5"] = QT_API_PYSIDE2 |
| 38 | +elif "PyQt4" in sys.modules: |
| 39 | + QT_API = rcParams["backend.qt4"] = QT_API_PYQTv2 |
| 40 | +elif "PySide" in sys.modules: |
| 41 | + QT_API = rcParams["backend.qt4"] = QT_API_PYSIDE |
| 42 | +# Otherwise, check the QT_API environment variable (from Enthought). This can |
| 43 | +# only override the binding, not the backend (in other words, we check that the |
| 44 | +# requested backend actually matches). |
| 45 | +elif rcParams["backend"] == "Qt5Agg": |
| 46 | + if QT_API_ENV == "pyqt5": |
| 47 | + rcParams["backend.qt5"] = QT_API_PYQT5 |
| 48 | + elif QT_API_ENV == "pyside2": |
| 49 | + rcParams["backend.qt5"] = QT_API_PYSIDE2 |
| 50 | + QT_API = rcParams["backend.qt5"] |
| 51 | +elif rcParams["backend"] == "Qt4Agg": |
| 52 | + if QT_API_ENV == "pyqt4": |
| 53 | + rcParams["backend.qt4"] = QT_API_PYQTv2 |
| 54 | + elif QT_API_ENV == "pyside": |
| 55 | + rcParams["backend.qt4"] = QT_API_PYSIDE |
| 56 | + QT_API = rcParams["backend.qt5"] |
| 57 | +# A non-Qt backend was selected but we still got there (possible, e.g., when |
| 58 | +# fully manually embedding Matplotlib in a Qt app without using pyplot). |
36 | 59 | else:
|
37 |
| - # A different backend was specified, but we still got here because a Qt |
38 |
| - # related file was imported. This is allowed, so lets try and guess |
39 |
| - # what we should be using. |
40 |
| - if "PyQt4" in sys.modules or "PySide" in sys.modules: |
41 |
| - # PyQt4 or PySide is actually used. |
42 |
| - QT_RC_MAJOR_VERSION = 4 |
43 |
| - else: |
44 |
| - # This is a fallback: PyQt5 |
45 |
| - QT_RC_MAJOR_VERSION = 5 |
46 |
| - |
47 |
| -QT_API = None |
48 |
| - |
49 |
| -# check if any binding is already imported, if so silently ignore the |
50 |
| -# rcparams/ENV settings and use what ever is already imported. |
51 |
| -if 'PySide' in sys.modules: |
52 |
| - # user has imported PySide before importing mpl |
53 |
| - QT_API = QT_API_PYSIDE |
54 |
| - |
55 |
| -if 'PySide2' in sys.modules: |
56 |
| - # user has imported PySide before importing mpl |
57 |
| - QT_API = QT_API_PYSIDE2 |
| 60 | + QT_API = None |
58 | 61 |
|
59 |
| -if 'PyQt4' in sys.modules: |
60 |
| - # user has imported PyQt4 before importing mpl |
61 |
| - # this case also handles the PyQt4v2 case as once sip is imported |
62 |
| - # the API versions can not be changed so do not try |
63 |
| - QT_API = QT_API_PYQT |
64 | 62 |
|
65 |
| -if 'PyQt5' in sys.modules: |
66 |
| - # the user has imported PyQt5 before importing mpl |
67 |
| - QT_API = QT_API_PYQT5 |
| 63 | +def _setup_pyqt4(): |
| 64 | + global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, _getSaveFileName |
68 | 65 |
|
69 |
| -if (QT_API_ENV is not None) and QT_API is None: |
70 |
| - try: |
71 |
| - QT_ENV_MAJOR_VERSION = ETS[QT_API_ENV][1] |
72 |
| - except KeyError: |
73 |
| - raise RuntimeError( |
74 |
| - ('Unrecognized environment variable %r, valid values are:' |
75 |
| - ' %r, %r, %r or %r' |
76 |
| - % (QT_API_ENV, 'pyqt', 'pyside', 'pyqt5', 'pyside2'))) |
77 |
| - if QT_ENV_MAJOR_VERSION == QT_RC_MAJOR_VERSION: |
78 |
| - # Only if backend and env qt major version are |
79 |
| - # compatible use the env variable. |
80 |
| - QT_API = ETS[QT_API_ENV][0] |
81 |
| - |
82 |
| -_fallback_to_qt4 = False |
83 |
| -if QT_API is None: |
84 |
| - # No ETS environment or incompatible so use rcParams. |
85 |
| - if rcParams['backend'] == 'Qt5Agg': |
86 |
| - QT_API = rcParams['backend.qt5'] |
87 |
| - elif rcParams['backend'] == 'Qt4Agg': |
88 |
| - QT_API = rcParams['backend.qt4'] |
89 |
| - else: |
90 |
| - # A non-Qt backend was specified, no version of the Qt |
91 |
| - # bindings is imported, but we still got here because a Qt |
92 |
| - # related file was imported. This is allowed, fall back to Qt5 |
93 |
| - # using which ever binding the rparams ask for. |
94 |
| - _fallback_to_qt4 = True |
95 |
| - QT_API = rcParams['backend.qt5'] |
96 |
| - |
97 |
| -# We will define an appropriate wrapper for the differing versions |
98 |
| -# of file dialog. |
99 |
| -_getSaveFileName = None |
100 |
| - |
101 |
| -# Flag to check if sip could be imported |
102 |
| -_sip_imported = False |
103 |
| - |
104 |
| -# Now perform the imports. |
105 |
| -if QT_API in (QT_API_PYQT, QT_API_PYQTv2, QT_API_PYQT5): |
106 |
| - try: |
| 66 | + def _setup_pyqt4(api): |
| 67 | + global QtCore, QtGui, QtWidgets, \ |
| 68 | + __version__, is_pyqt5, _getSaveFileName |
| 69 | + # List of incompatible APIs: |
| 70 | + # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html |
| 71 | + _sip_apis = ["QDate", "QDateTime", "QString", "QTextStream", "QTime", |
| 72 | + "QUrl", "QVariant"] |
107 | 73 | import sip
|
108 |
| - _sip_imported = True |
109 |
| - except ImportError: |
110 |
| - # Try using PySide |
111 |
| - if QT_RC_MAJOR_VERSION == 5: |
112 |
| - QT_API = QT_API_PYSIDE2 |
113 |
| - else: |
114 |
| - QT_API = QT_API_PYSIDE |
115 |
| - cond = ("Could not import sip; falling back on PySide\n" |
116 |
| - "in place of PyQt4 or PyQt5.\n") |
117 |
| - _log.info(cond) |
118 |
| - |
119 |
| -if _sip_imported: |
120 |
| - if QT_API == QT_API_PYQTv2: |
121 |
| - if QT_API_ENV == 'pyqt': |
122 |
| - cond = ("Found 'QT_API=pyqt' environment variable. " |
123 |
| - "Setting PyQt4 API accordingly.\n") |
124 |
| - else: |
125 |
| - cond = "PyQt API v2 specified." |
126 |
| - try: |
127 |
| - sip.setapi('QString', 2) |
128 |
| - except: |
129 |
| - res = 'QString API v2 specification failed. Defaulting to v1.' |
130 |
| - _log.info(cond + res) |
131 |
| - # condition has now been reported, no need to repeat it: |
132 |
| - cond = "" |
133 |
| - try: |
134 |
| - sip.setapi('QVariant', 2) |
135 |
| - except: |
136 |
| - res = 'QVariant API v2 specification failed. Defaulting to v1.' |
137 |
| - _log.info(cond + res) |
138 |
| - if QT_API == QT_API_PYQT5: |
139 |
| - try: |
140 |
| - from PyQt5 import QtCore, QtGui, QtWidgets |
141 |
| - _getSaveFileName = QtWidgets.QFileDialog.getSaveFileName |
142 |
| - except ImportError: |
143 |
| - if _fallback_to_qt4: |
144 |
| - # fell through, tried PyQt5, failed fall back to PyQt4 |
145 |
| - QT_API = rcParams['backend.qt4'] |
146 |
| - QT_RC_MAJOR_VERSION = 4 |
147 |
| - else: |
148 |
| - raise |
149 |
| - |
150 |
| - # needs to be if so we can re-test the value of QT_API which may |
151 |
| - # have been changed in the above if block |
152 |
| - if QT_API in [QT_API_PYQT, QT_API_PYQTv2]: # PyQt4 API |
| 74 | + for _sip_api in _sip_apis: |
| 75 | + try: |
| 76 | + sip.setapi(_sip_api, api) |
| 77 | + except ValueError: |
| 78 | + pass |
153 | 79 | from PyQt4 import QtCore, QtGui
|
| 80 | + __version__ = QtCore.PYQT_VERSION_STR |
| 81 | + # PyQt 4.6 introduced getSaveFileNameAndFilter: |
| 82 | + # https://riverbankcomputing.com/news/pyqt-46 |
| 83 | + if __version__ < LooseVersion(str("4.6")): |
| 84 | + raise ImportError("PyQt<4.6 is not supported") |
| 85 | + QtCore.Signal = QtCore.pyqtSignal |
| 86 | + QtCore.Slot = QtCore.pyqtSlot |
| 87 | + QtCore.Property = QtCore.pyqtProperty |
| 88 | + _getSaveFileName = QtGui.QFileDialog.getSaveFileNameAndFilter |
154 | 89 |
|
155 |
| - try: |
156 |
| - if sip.getapi("QString") > 1: |
157 |
| - # Use new getSaveFileNameAndFilter() |
158 |
| - _getSaveFileName = QtGui.QFileDialog.getSaveFileNameAndFilter |
159 |
| - else: |
| 90 | + if QT_API == QT_API_PYQT: |
| 91 | + _setup_pyqt4(api=1) |
| 92 | + elif QT_API == QT_API_PYQTv2: |
| 93 | + _setup_pyqt4(api=2) |
| 94 | + elif QT_API == QT_API_PYSIDE: |
| 95 | + from PySide import QtCore, QtGui, __version__, __version_info__ |
| 96 | + # PySide 1.0.3 fixed the following: |
| 97 | + # https://srinikom.github.io/pyside-bz-archive/809.html |
| 98 | + if __version_info__ < (1, 0, 3): |
| 99 | + raise ImportError("PySide<1.0.3 is not supported") |
| 100 | + _getSaveFileName = QtGui.QFileDialog.getSaveFileName |
| 101 | + else: |
| 102 | + raise ValueError('Unexpected value for the "backend.qt4" rcparam') |
| 103 | + QtWidgets = QtGui |
160 | 104 |
|
161 |
| - # Use old getSaveFileName() |
162 |
| - def _getSaveFileName(*args, **kwargs): |
163 |
| - return (QtGui.QFileDialog.getSaveFileName(*args, **kwargs), |
164 |
| - None) |
| 105 | + def is_pyqt5(): |
| 106 | + return False |
165 | 107 |
|
166 |
| - except (AttributeError, KeyError): |
167 | 108 |
|
168 |
| - # call to getapi() can fail in older versions of sip |
169 |
| - def _getSaveFileName(*args, **kwargs): |
170 |
| - return QtGui.QFileDialog.getSaveFileName(*args, **kwargs), None |
171 |
| - try: |
172 |
| - # Alias PyQt-specific functions for PySide compatibility. |
173 |
| - QtCore.Signal = QtCore.pyqtSignal |
174 |
| - try: |
175 |
| - QtCore.Slot = QtCore.pyqtSlot |
176 |
| - except AttributeError: |
177 |
| - # Not a perfect match but works in simple cases |
178 |
| - QtCore.Slot = QtCore.pyqtSignature |
| 109 | +def _setup_pyqt5(): |
| 110 | + global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, _getSaveFileName |
179 | 111 |
|
180 |
| - QtCore.Property = QtCore.pyqtProperty |
| 112 | + if QT_API == QT_API_PYQT5: |
| 113 | + from PyQt5 import QtCore, QtGui, QtWidgets |
181 | 114 | __version__ = QtCore.PYQT_VERSION_STR
|
182 |
| - except NameError: |
183 |
| - # QtCore did not get imported, fall back to pyside |
184 |
| - if QT_RC_MAJOR_VERSION == 5: |
185 |
| - QT_API = QT_API_PYSIDE2 |
186 |
| - else: |
187 |
| - QT_API = QT_API_PYSIDE |
| 115 | + QtCore.Signal = QtCore.pyqtSignal |
| 116 | + QtCore.Slot = QtCore.pyqtSlot |
| 117 | + QtCore.Property = QtCore.pyqtProperty |
| 118 | + elif QT_API == QT_API_PYSIDE2: |
| 119 | + from PySide2 import QtCore, QtGui, QtWidgets, __version__ |
| 120 | + else: |
| 121 | + raise ValueError('Unexpected value for the "backend.qt5" rcparam') |
| 122 | + _getSaveFileName = QtWidgets.QFileDialog.getSaveFileName |
188 | 123 |
|
| 124 | + def is_pyqt5(): |
| 125 | + return True |
189 | 126 |
|
190 |
| -if QT_API == QT_API_PYSIDE2: |
191 |
| - try: |
192 |
| - from PySide2 import QtCore, QtGui, QtWidgets, __version__ |
193 |
| - _getSaveFileName = QtWidgets.QFileDialog.getSaveFileName |
194 |
| - except ImportError: |
195 |
| - # tried PySide2, failed, fall back to PySide |
196 |
| - QT_RC_MAJOR_VERSION = 4 |
197 |
| - QT_API = QT_API_PYSIDE |
198 | 127 |
|
199 |
| -if QT_API == QT_API_PYSIDE: # try importing pyside |
| 128 | +if QT_API in [QT_API_PYQT5, QT_API_PYSIDE2]: |
| 129 | + _setup_pyqt5() |
| 130 | +elif QT_API in [QT_API_PYQT, QT_API_PYQTv2, QT_API_PYSIDE]: |
| 131 | + _setup_pyqt4() |
| 132 | +elif QT_API is None: |
200 | 133 | try:
|
201 |
| - from PySide import QtCore, QtGui, __version__, __version_info__ |
| 134 | + _setup_pyqt5() |
202 | 135 | except ImportError:
|
203 |
| - raise ImportError( |
204 |
| - "Matplotlib qt-based backends require an external PyQt4, PyQt5,\n" |
205 |
| - "PySide or PySide2 package to be installed, but it was not found.") |
206 |
| - |
207 |
| - if __version_info__ < (1, 0, 3): |
208 |
| - raise ImportError( |
209 |
| - "Matplotlib backend_qt4 and backend_qt4agg require PySide >=1.0.3") |
210 |
| - |
211 |
| - _getSaveFileName = QtGui.QFileDialog.getSaveFileName |
212 |
| - |
213 |
| - |
214 |
| -# Apply shim to Qt4 APIs to make them look like Qt5 |
215 |
| -if QT_API in (QT_API_PYQT, QT_API_PYQTv2, QT_API_PYSIDE): |
216 |
| - '''Import all used QtGui objects into QtWidgets |
217 |
| -
|
218 |
| - Here I've opted to simple copy QtGui into QtWidgets as that |
219 |
| - achieves the same result as copying over the objects, and will |
220 |
| - continue to work if other objects are used. |
221 |
| -
|
222 |
| - ''' |
223 |
| - QtWidgets = QtGui |
| 136 | + _setup_pyqt4() |
| 137 | +else: |
| 138 | + raise RuntimeError # We should not get there. |
224 | 139 |
|
225 | 140 |
|
226 |
| -def is_pyqt5(): |
227 |
| - return QT_API == QT_API_PYQT5 |
| 141 | +# These globals are only defined for backcompatibilty purposes. |
| 142 | +ETS = dict(pyqt=(QT_API_PYQTv2, 4), pyside=(QT_API_PYSIDE, 4), |
| 143 | + pyqt5=(QT_API_PYQT5, 5), pyside2=(QT_API_PYSIDE2, 5)) |
| 144 | +QT_RC_MAJOR_VERSION = 5 if is_pyqt5() else 4 |
0 commit comments