Skip to content

Commit 55a9d88

Browse files
committed
Rewrite and greatly simplify qt_compat.py.
The selection logic is now described in the module's docstring. The only changes is that the QT_ENV_MAJOR_VERSION global, which would sometimes be defined (depending on the state of the import cache, the QT_API environment variable, and the requested backend) is never defined anymore.
1 parent cfb648f commit 55a9d88

File tree

4 files changed

+123
-199
lines changed

4 files changed

+123
-199
lines changed

INSTALL.rst

+3-2
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,9 @@ interface toolkits. See :ref:`what-is-a-backend` for more details on the
189189
optional Matplotlib backends and the capabilities they provide.
190190

191191
* :term:`tk` (>= 8.3, != 8.6.0 or 8.6.1): for the TkAgg backend;
192-
* `PyQt4 <https://pypi.python.org/pypi/PyQt4>`_ (>= 4.4) or
193-
`PySide <https://pypi.python.org/pypi/PySide>`_: for the Qt4Agg backend;
192+
* `PyQt4 <https://pypi.python.org/pypi/PyQt4>`_ (>= 4.6) or
193+
`PySide <https://pypi.python.org/pypi/PySide>`_ (>= 1.0.3): for the Qt4Agg
194+
backend;
194195
* `PyQt5 <https://pypi.python.org/pypi/PyQt5>`_: for the Qt5Agg backend;
195196
* :term:`pygtk` (>= 2.4): for the GTK and the GTKAgg backend;
196197
* :term:`wxpython` (>= 2.9 or later): for the WX or WXAgg backend;

doc/conf.py

+6
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,9 @@ class Frame(object):
355355

356356

357357
class MyPyQt4(MagicMock):
358+
class QtCore(MagicMock):
359+
PYQT_VERSION_STR = "4.6"
360+
358361
class QtGui(object):
359362
# PyQt4.QtGui public classes.
360363
# Generated with
@@ -450,6 +453,9 @@ class QtGui(object):
450453
locals()[_name] = type(_name, (), {})
451454
del _name
452455

456+
QtCore = QtCore()
457+
QtGui = QtGui()
458+
453459

454460
class MySip(MagicMock):
455461
def getapi(*args):

lib/matplotlib/backends/qt_compat.py

+113-196
Original file line numberDiff line numberDiff line change
@@ -1,227 +1,144 @@
1-
""" A Qt API selector that can be used to switch between PyQt and PySide.
21
"""
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+
315
from __future__ import (absolute_import, division, print_function,
416
unicode_literals)
517

618
import six
719

20+
from distutils.version import LooseVersion
821
import os
9-
import logging
1022
import sys
11-
from matplotlib import rcParams
1223

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
2125

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.
2926

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"
3032
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).
3659
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
5861

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
6462

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
6865

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"]
10773
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
15379
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
15489

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
160104

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
165107

166-
except (AttributeError, KeyError):
167108

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
179111

180-
QtCore.Property = QtCore.pyqtProperty
112+
if QT_API == QT_API_PYQT5:
113+
from PyQt5 import QtCore, QtGui, QtWidgets
181114
__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
188123

124+
def is_pyqt5():
125+
return True
189126

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
198127

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:
200133
try:
201-
from PySide import QtCore, QtGui, __version__, __version_info__
134+
_setup_pyqt5()
202135
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.
224139

225140

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

lib/matplotlib/backends/qt_editor/formlayout.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
import six
5454

5555
from matplotlib import colors as mcolors
56-
from matplotlib.backends.qt_compat import QtGui, QtWidgets, QtCore
56+
from ..qt_compat import QtCore, QtGui, QtWidgets
5757

5858

5959
BLACKLIST = {"title", "label"}

0 commit comments

Comments
 (0)