|
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 | importsix
|
7 | 19 |
|
| 20 | +fromdistutils.versionimportLooseVersion |
8 | 21 | importos
|
9 |
| -importlogging |
10 | 22 | importsys
|
11 |
| -frommatplotlibimportrcParams |
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 | +frommatplotlibimportrcParams |
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 |
| -ifrcParams['backend']=='Qt5Agg': |
33 |
| -QT_RC_MAJOR_VERSION=5 |
34 |
| -elifrcParams['backend']=='Qt4Agg': |
35 |
| -QT_RC_MAJOR_VERSION=4 |
| 33 | +# First, check if anything is already imported. |
| 34 | +if"PyQt5"insys.modules: |
| 35 | +QT_API=rcParams["backend.qt5"]=QT_API_PYQT5 |
| 36 | +elif"PySide2"insys.modules: |
| 37 | +QT_API=rcParams["backend.qt5"]=QT_API_PYSIDE2 |
| 38 | +elif"PyQt4"insys.modules: |
| 39 | +QT_API=rcParams["backend.qt4"]=QT_API_PYQTv2 |
| 40 | +elif"PySide"insys.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 | +elifrcParams["backend"]=="Qt5Agg": |
| 46 | +ifQT_API_ENV=="pyqt5": |
| 47 | +rcParams["backend.qt5"]=QT_API_PYQT5 |
| 48 | +elifQT_API_ENV=="pyside2": |
| 49 | +rcParams["backend.qt5"]=QT_API_PYSIDE2 |
| 50 | +QT_API=rcParams["backend.qt5"] |
| 51 | +elifrcParams["backend"]=="Qt4Agg": |
| 52 | +ifQT_API_ENV=="pyqt4": |
| 53 | +rcParams["backend.qt4"]=QT_API_PYQTv2 |
| 54 | +elifQT_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"insys.modulesor"PySide"insys.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'insys.modules: |
52 |
| -# user has imported PySide before importing mpl |
53 |
| -QT_API=QT_API_PYSIDE |
54 |
| - |
55 |
| -if'PySide2'insys.modules: |
56 |
| -# user has imported PySide before importing mpl |
57 |
| -QT_API=QT_API_PYSIDE2 |
| 60 | +QT_API=None |
58 | 61 |
|
59 |
| -if'PyQt4'insys.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'insys.modules: |
66 |
| -# the user has imported PyQt5 before importing mpl |
67 |
| -QT_API=QT_API_PYQT5 |
| 63 | +def_setup_pyqt4(): |
| 64 | +globalQtCore,QtGui,QtWidgets,__version__,is_pyqt5,_getSaveFileName |
68 | 65 |
|
69 |
| -if (QT_API_ENVisnotNone)andQT_APIisNone: |
70 |
| -try: |
71 |
| -QT_ENV_MAJOR_VERSION=ETS[QT_API_ENV][1] |
72 |
| -exceptKeyError: |
73 |
| -raiseRuntimeError( |
74 |
| - ('Unrecognized environment variable %r, valid values are:' |
75 |
| -' %r, %r, %r or %r' |
76 |
| -% (QT_API_ENV,'pyqt','pyside','pyqt5','pyside2'))) |
77 |
| -ifQT_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 |
| -ifQT_APIisNone: |
84 |
| -# No ETS environment or incompatible so use rcParams. |
85 |
| -ifrcParams['backend']=='Qt5Agg': |
86 |
| -QT_API=rcParams['backend.qt5'] |
87 |
| -elifrcParams['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 |
| -ifQT_APIin (QT_API_PYQT,QT_API_PYQTv2,QT_API_PYQT5): |
106 |
| -try: |
| 66 | +def_setup_pyqt4(api): |
| 67 | +globalQtCore,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 | importsip
|
108 |
| -_sip_imported=True |
109 |
| -exceptImportError: |
110 |
| -# Try using PySide |
111 |
| -ifQT_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 |
| -ifQT_API==QT_API_PYQTv2: |
121 |
| -ifQT_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 |
| -ifQT_API==QT_API_PYQT5: |
139 |
| -try: |
140 |
| -fromPyQt5importQtCore,QtGui,QtWidgets |
141 |
| -_getSaveFileName=QtWidgets.QFileDialog.getSaveFileName |
142 |
| -exceptImportError: |
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 |
| -ifQT_APIin [QT_API_PYQT,QT_API_PYQTv2]:# PyQt4 API |
| 74 | +for_sip_apiin_sip_apis: |
| 75 | +try: |
| 76 | +sip.setapi(_sip_api,api) |
| 77 | +exceptValueError: |
| 78 | +pass |
153 | 79 | fromPyQt4importQtCore,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 | +raiseImportError("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 |
| -ifsip.getapi("QString")>1: |
157 |
| -# Use new getSaveFileNameAndFilter() |
158 |
| -_getSaveFileName=QtGui.QFileDialog.getSaveFileNameAndFilter |
159 |
| -else: |
| 90 | +ifQT_API==QT_API_PYQT: |
| 91 | +_setup_pyqt4(api=1) |
| 92 | +elifQT_API==QT_API_PYQTv2: |
| 93 | +_setup_pyqt4(api=2) |
| 94 | +elifQT_API==QT_API_PYSIDE: |
| 95 | +fromPySideimportQtCore,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 | +raiseImportError("PySide<1.0.3 is not supported") |
| 100 | +_getSaveFileName=QtGui.QFileDialog.getSaveFileName |
| 101 | +else: |
| 102 | +raiseValueError('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 | +defis_pyqt5(): |
| 106 | +returnFalse |
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 |
| -returnQtGui.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 |
| -exceptAttributeError: |
177 |
| -# Not a perfect match but works in simple cases |
178 |
| -QtCore.Slot=QtCore.pyqtSignature |
| 109 | +def_setup_pyqt5(): |
| 110 | +globalQtCore,QtGui,QtWidgets,__version__,is_pyqt5,_getSaveFileName |
179 | 111 |
|
180 |
| -QtCore.Property=QtCore.pyqtProperty |
| 112 | +ifQT_API==QT_API_PYQT5: |
| 113 | +fromPyQt5importQtCore,QtGui,QtWidgets |
181 | 114 | __version__=QtCore.PYQT_VERSION_STR
|
182 |
| -exceptNameError: |
183 |
| -# QtCore did not get imported, fall back to pyside |
184 |
| -ifQT_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 | +elifQT_API==QT_API_PYSIDE2: |
| 119 | +fromPySide2importQtCore,QtGui,QtWidgets,__version__ |
| 120 | +else: |
| 121 | +raiseValueError('Unexpected value for the "backend.qt5" rcparam') |
| 122 | +_getSaveFileName=QtWidgets.QFileDialog.getSaveFileName |
188 | 123 |
|
| 124 | +defis_pyqt5(): |
| 125 | +returnTrue |
189 | 126 |
|
190 |
| -ifQT_API==QT_API_PYSIDE2: |
191 |
| -try: |
192 |
| -fromPySide2importQtCore,QtGui,QtWidgets,__version__ |
193 |
| -_getSaveFileName=QtWidgets.QFileDialog.getSaveFileName |
194 |
| -exceptImportError: |
195 |
| -# tried PySide2, failed, fall back to PySide |
196 |
| -QT_RC_MAJOR_VERSION=4 |
197 |
| -QT_API=QT_API_PYSIDE |
198 | 127 |
|
199 |
| -ifQT_API==QT_API_PYSIDE:# try importing pyside |
| 128 | +ifQT_APIin [QT_API_PYQT5,QT_API_PYSIDE2]: |
| 129 | +_setup_pyqt5() |
| 130 | +elifQT_APIin [QT_API_PYQT,QT_API_PYQTv2,QT_API_PYSIDE]: |
| 131 | +_setup_pyqt4() |
| 132 | +elifQT_APIisNone: |
200 | 133 | try:
|
201 |
| -fromPySideimportQtCore,QtGui,__version__,__version_info__ |
| 134 | +_setup_pyqt5() |
202 | 135 | exceptImportError:
|
203 |
| -raiseImportError( |
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 |
| -raiseImportError( |
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 |
| -ifQT_APIin (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 | +raiseRuntimeError# We should not get there. |
224 | 139 |
|
225 | 140 |
|
226 |
| -defis_pyqt5(): |
227 |
| -returnQT_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=5ifis_pyqt5()else4 |