qt_compat.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. """
  2. Qt binding and backend selector.
  3. The selection logic is as follows:
  4. - if any of PyQt5, PySide2, PyQt4 or PySide have already been imported
  5. (checked in that order), use it;
  6. - otherwise, if the QT_API environment variable (used by Enthought) is set, use
  7. it to determine which binding to use (but do not change the backend based on
  8. it; i.e. if the Qt5Agg backend is requested but QT_API is set to "pyqt4",
  9. then actually use Qt5 with PyQt5 or PySide2 (whichever can be imported);
  10. - otherwise, use whatever the rcParams indicate.
  11. """
  12. from distutils.version import LooseVersion
  13. import os
  14. import sys
  15. from matplotlib import rcParams
  16. QT_API_PYQT5 = "PyQt5"
  17. QT_API_PYSIDE2 = "PySide2"
  18. QT_API_PYQTv2 = "PyQt4v2"
  19. QT_API_PYSIDE = "PySide"
  20. QT_API_PYQT = "PyQt4" # Use the old sip v1 API (Py3 defaults to v2).
  21. QT_API_ENV = os.environ.get("QT_API")
  22. # Mapping of QT_API_ENV to requested binding. ETS does not support PyQt4v1.
  23. # (https://github.com/enthought/pyface/blob/master/pyface/qt/__init__.py)
  24. _ETS = {"pyqt5": QT_API_PYQT5, "pyside2": QT_API_PYSIDE2,
  25. "pyqt": QT_API_PYQTv2, "pyside": QT_API_PYSIDE,
  26. None: None}
  27. # First, check if anything is already imported.
  28. if "PyQt5.QtCore" in sys.modules:
  29. QT_API = QT_API_PYQT5
  30. elif "PySide2.QtCore" in sys.modules:
  31. QT_API = QT_API_PYSIDE2
  32. elif "PyQt4.QtCore" in sys.modules:
  33. QT_API = QT_API_PYQTv2
  34. elif "PySide.QtCore" in sys.modules:
  35. QT_API = QT_API_PYSIDE
  36. # Otherwise, check the QT_API environment variable (from Enthought). This can
  37. # only override the binding, not the backend (in other words, we check that the
  38. # requested backend actually matches).
  39. elif rcParams["backend"] in ["Qt5Agg", "Qt5Cairo"]:
  40. if QT_API_ENV in ["pyqt5", "pyside2"]:
  41. QT_API = _ETS[QT_API_ENV]
  42. else:
  43. QT_API = None
  44. elif rcParams["backend"] in ["Qt4Agg", "Qt4Cairo"]:
  45. if QT_API_ENV in ["pyqt4", "pyside"]:
  46. QT_API = _ETS[QT_API_ENV]
  47. else:
  48. QT_API = None
  49. # A non-Qt backend was selected but we still got there (possible, e.g., when
  50. # fully manually embedding Matplotlib in a Qt app without using pyplot).
  51. else:
  52. try:
  53. QT_API = _ETS[QT_API_ENV]
  54. except KeyError:
  55. raise RuntimeError(
  56. "The environment variable QT_API has the unrecognized value {!r};"
  57. "valid values are 'pyqt5', 'pyside2', 'pyqt', and 'pyside'")
  58. def _setup_pyqt5():
  59. global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, \
  60. _isdeleted, _getSaveFileName
  61. if QT_API == QT_API_PYQT5:
  62. from PyQt5 import QtCore, QtGui, QtWidgets
  63. import sip
  64. __version__ = QtCore.PYQT_VERSION_STR
  65. QtCore.Signal = QtCore.pyqtSignal
  66. QtCore.Slot = QtCore.pyqtSlot
  67. QtCore.Property = QtCore.pyqtProperty
  68. _isdeleted = sip.isdeleted
  69. elif QT_API == QT_API_PYSIDE2:
  70. from PySide2 import QtCore, QtGui, QtWidgets, __version__
  71. import shiboken2
  72. def _isdeleted(obj): return not shiboken2.isValid(obj)
  73. else:
  74. raise ValueError("Unexpected value for the 'backend.qt5' rcparam")
  75. _getSaveFileName = QtWidgets.QFileDialog.getSaveFileName
  76. def is_pyqt5():
  77. return True
  78. def _setup_pyqt4():
  79. global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, \
  80. _isdeleted, _getSaveFileName
  81. def _setup_pyqt4_internal(api):
  82. global QtCore, QtGui, QtWidgets, \
  83. __version__, is_pyqt5, _getSaveFileName
  84. # List of incompatible APIs:
  85. # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html
  86. _sip_apis = ["QDate", "QDateTime", "QString", "QTextStream", "QTime",
  87. "QUrl", "QVariant"]
  88. try:
  89. import sip
  90. except ImportError:
  91. pass
  92. else:
  93. for _sip_api in _sip_apis:
  94. try:
  95. sip.setapi(_sip_api, api)
  96. except ValueError:
  97. pass
  98. from PyQt4 import QtCore, QtGui
  99. import sip # Always succeeds *after* importing PyQt4.
  100. __version__ = QtCore.PYQT_VERSION_STR
  101. # PyQt 4.6 introduced getSaveFileNameAndFilter:
  102. # https://riverbankcomputing.com/news/pyqt-46
  103. if __version__ < LooseVersion("4.6"):
  104. raise ImportError("PyQt<4.6 is not supported")
  105. QtCore.Signal = QtCore.pyqtSignal
  106. QtCore.Slot = QtCore.pyqtSlot
  107. QtCore.Property = QtCore.pyqtProperty
  108. _isdeleted = sip.isdeleted
  109. _getSaveFileName = QtGui.QFileDialog.getSaveFileNameAndFilter
  110. if QT_API == QT_API_PYQTv2:
  111. _setup_pyqt4_internal(api=2)
  112. elif QT_API == QT_API_PYSIDE:
  113. from PySide import QtCore, QtGui, __version__, __version_info__
  114. import shiboken
  115. # PySide 1.0.3 fixed the following:
  116. # https://srinikom.github.io/pyside-bz-archive/809.html
  117. if __version_info__ < (1, 0, 3):
  118. raise ImportError("PySide<1.0.3 is not supported")
  119. def _isdeleted(obj): return not shiboken.isValid(obj)
  120. _getSaveFileName = QtGui.QFileDialog.getSaveFileName
  121. elif QT_API == QT_API_PYQT:
  122. _setup_pyqt4_internal(api=1)
  123. else:
  124. raise ValueError("Unexpected value for the 'backend.qt4' rcparam")
  125. QtWidgets = QtGui
  126. def is_pyqt5():
  127. return False
  128. if QT_API in [QT_API_PYQT5, QT_API_PYSIDE2]:
  129. _setup_pyqt5()
  130. elif QT_API in [QT_API_PYQTv2, QT_API_PYSIDE, QT_API_PYQT]:
  131. _setup_pyqt4()
  132. elif QT_API is None:
  133. if rcParams["backend"] == "Qt4Agg":
  134. _candidates = [(_setup_pyqt4, QT_API_PYQTv2),
  135. (_setup_pyqt4, QT_API_PYSIDE),
  136. (_setup_pyqt4, QT_API_PYQT),
  137. (_setup_pyqt5, QT_API_PYQT5),
  138. (_setup_pyqt5, QT_API_PYSIDE2)]
  139. else:
  140. _candidates = [(_setup_pyqt5, QT_API_PYQT5),
  141. (_setup_pyqt5, QT_API_PYSIDE2),
  142. (_setup_pyqt4, QT_API_PYQTv2),
  143. (_setup_pyqt4, QT_API_PYSIDE),
  144. (_setup_pyqt4, QT_API_PYQT)]
  145. for _setup, QT_API in _candidates:
  146. try:
  147. _setup()
  148. except ImportError:
  149. continue
  150. break
  151. else:
  152. raise ImportError("Failed to import any qt binding")
  153. else: # We should not get there.
  154. raise AssertionError("Unexpected QT_API: {}".format(QT_API))
  155. # These globals are only defined for backcompatibility purposes.
  156. ETS = dict(pyqt=(QT_API_PYQTv2, 4), pyside=(QT_API_PYSIDE, 4),
  157. pyqt5=(QT_API_PYQT5, 5), pyside2=(QT_API_PYSIDE2, 5))
  158. QT_RC_MAJOR_VERSION = 5 if is_pyqt5() else 4