qt_compat.py 6.4 KB
Newer Older
Stelios Karozis's avatar
Stelios Karozis committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
"""
Qt binding and backend selector.

The selection logic is as follows:
- if any of PyQt5, PySide2, PyQt4 or PySide have already been imported
  (checked in that order), use it;
- otherwise, if the QT_API environment variable (used by Enthought) is set, use
  it to determine which binding to use (but do not change the backend based on
  it; i.e. if the Qt5Agg backend is requested but QT_API is set to "pyqt4",
  then actually use Qt5 with PyQt5 or PySide2 (whichever can be imported);
- otherwise, use whatever the rcParams indicate.
"""

from distutils.version import LooseVersion
import os
import sys

from matplotlib import rcParams


QT_API_PYQT5 = "PyQt5"
QT_API_PYSIDE2 = "PySide2"
QT_API_PYQTv2 = "PyQt4v2"
QT_API_PYSIDE = "PySide"
QT_API_PYQT = "PyQt4"   # Use the old sip v1 API (Py3 defaults to v2).
QT_API_ENV = os.environ.get("QT_API")
# Mapping of QT_API_ENV to requested binding.  ETS does not support PyQt4v1.
# (https://github.com/enthought/pyface/blob/master/pyface/qt/__init__.py)
_ETS = {"pyqt5": QT_API_PYQT5, "pyside2": QT_API_PYSIDE2,
        "pyqt": QT_API_PYQTv2, "pyside": QT_API_PYSIDE,
        None: None}
# First, check if anything is already imported.
if "PyQt5.QtCore" in sys.modules:
    QT_API = QT_API_PYQT5
elif "PySide2.QtCore" in sys.modules:
    QT_API = QT_API_PYSIDE2
elif "PyQt4.QtCore" in sys.modules:
    QT_API = QT_API_PYQTv2
elif "PySide.QtCore" in sys.modules:
    QT_API = QT_API_PYSIDE
# Otherwise, check the QT_API environment variable (from Enthought).  This can
# only override the binding, not the backend (in other words, we check that the
# requested backend actually matches).
elif rcParams["backend"] in ["Qt5Agg", "Qt5Cairo"]:
    if QT_API_ENV in ["pyqt5", "pyside2"]:
        QT_API = _ETS[QT_API_ENV]
    else:
        QT_API = None
elif rcParams["backend"] in ["Qt4Agg", "Qt4Cairo"]:
    if QT_API_ENV in ["pyqt4", "pyside"]:
        QT_API = _ETS[QT_API_ENV]
    else:
        QT_API = None
# A non-Qt backend was selected but we still got there (possible, e.g., when
# fully manually embedding Matplotlib in a Qt app without using pyplot).
else:
    try:
        QT_API = _ETS[QT_API_ENV]
    except KeyError:
        raise RuntimeError(
            "The environment variable QT_API has the unrecognized value {!r};"
            "valid values are 'pyqt5', 'pyside2', 'pyqt', and 'pyside'")


def _setup_pyqt5():
    global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, \
        _isdeleted, _getSaveFileName

    if QT_API == QT_API_PYQT5:
        from PyQt5 import QtCore, QtGui, QtWidgets
        import sip
        __version__ = QtCore.PYQT_VERSION_STR
        QtCore.Signal = QtCore.pyqtSignal
        QtCore.Slot = QtCore.pyqtSlot
        QtCore.Property = QtCore.pyqtProperty
        _isdeleted = sip.isdeleted
    elif QT_API == QT_API_PYSIDE2:
        from PySide2 import QtCore, QtGui, QtWidgets, __version__
        import shiboken2
        def _isdeleted(obj): return not shiboken2.isValid(obj)
    else:
        raise ValueError("Unexpected value for the 'backend.qt5' rcparam")
    _getSaveFileName = QtWidgets.QFileDialog.getSaveFileName

    def is_pyqt5():
        return True


def _setup_pyqt4():
    global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, \
        _isdeleted, _getSaveFileName

    def _setup_pyqt4_internal(api):
        global QtCore, QtGui, QtWidgets, \
            __version__, is_pyqt5, _getSaveFileName
        # List of incompatible APIs:
        # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html
        _sip_apis = ["QDate", "QDateTime", "QString", "QTextStream", "QTime",
                     "QUrl", "QVariant"]
        try:
            import sip
        except ImportError:
            pass
        else:
            for _sip_api in _sip_apis:
                try:
                    sip.setapi(_sip_api, api)
                except ValueError:
                    pass
        from PyQt4 import QtCore, QtGui
        import sip  # Always succeeds *after* importing PyQt4.
        __version__ = QtCore.PYQT_VERSION_STR
        # PyQt 4.6 introduced getSaveFileNameAndFilter:
        # https://riverbankcomputing.com/news/pyqt-46
        if __version__ < LooseVersion("4.6"):
            raise ImportError("PyQt<4.6 is not supported")
        QtCore.Signal = QtCore.pyqtSignal
        QtCore.Slot = QtCore.pyqtSlot
        QtCore.Property = QtCore.pyqtProperty
        _isdeleted = sip.isdeleted
        _getSaveFileName = QtGui.QFileDialog.getSaveFileNameAndFilter

    if QT_API == QT_API_PYQTv2:
        _setup_pyqt4_internal(api=2)
    elif QT_API == QT_API_PYSIDE:
        from PySide import QtCore, QtGui, __version__, __version_info__
        import shiboken
        # PySide 1.0.3 fixed the following:
        # https://srinikom.github.io/pyside-bz-archive/809.html
        if __version_info__ < (1, 0, 3):
            raise ImportError("PySide<1.0.3 is not supported")
        def _isdeleted(obj): return not shiboken.isValid(obj)
        _getSaveFileName = QtGui.QFileDialog.getSaveFileName
    elif QT_API == QT_API_PYQT:
        _setup_pyqt4_internal(api=1)
    else:
        raise ValueError("Unexpected value for the 'backend.qt4' rcparam")
    QtWidgets = QtGui

    def is_pyqt5():
        return False


if QT_API in [QT_API_PYQT5, QT_API_PYSIDE2]:
    _setup_pyqt5()
elif QT_API in [QT_API_PYQTv2, QT_API_PYSIDE, QT_API_PYQT]:
    _setup_pyqt4()
elif QT_API is None:
    if rcParams["backend"] == "Qt4Agg":
        _candidates = [(_setup_pyqt4, QT_API_PYQTv2),
                       (_setup_pyqt4, QT_API_PYSIDE),
                       (_setup_pyqt4, QT_API_PYQT),
                       (_setup_pyqt5, QT_API_PYQT5),
                       (_setup_pyqt5, QT_API_PYSIDE2)]
    else:
        _candidates = [(_setup_pyqt5, QT_API_PYQT5),
                       (_setup_pyqt5, QT_API_PYSIDE2),
                       (_setup_pyqt4, QT_API_PYQTv2),
                       (_setup_pyqt4, QT_API_PYSIDE),
                       (_setup_pyqt4, QT_API_PYQT)]
    for _setup, QT_API in _candidates:
        try:
            _setup()
        except ImportError:
            continue
        break
    else:
        raise ImportError("Failed to import any qt binding")
else:  # We should not get there.
    raise AssertionError("Unexpected QT_API: {}".format(QT_API))


# These globals are only defined for backcompatibility purposes.
ETS = dict(pyqt=(QT_API_PYQTv2, 4), pyside=(QT_API_PYSIDE, 4),
           pyqt5=(QT_API_PYQT5, 5), pyside2=(QT_API_PYSIDE2, 5))
QT_RC_MAJOR_VERSION = 5 if is_pyqt5() else 4