""" Qt binding and backend selector. The selection logic is as follows: - if any of PyQt6, PySide6, PyQt5, or PySide2 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; - otherwise, use whatever the rcParams indicate. """ import operator import os import platform import sys from packaging.version import parse as parse_version import matplotlib as mpl from . import _QT_FORCE_QT5_BINDING QT_API_PYQT6 = "PyQt6" QT_API_PYSIDE6 = "PySide6" QT_API_PYQT5 = "PyQt5" QT_API_PYSIDE2 = "PySide2" QT_API_ENV = os.environ.get("QT_API") if QT_API_ENV is not None: QT_API_ENV = QT_API_ENV.lower() _ETS = { # Mapping of QT_API_ENV to requested binding. "pyqt6": QT_API_PYQT6, "pyside6": QT_API_PYSIDE6, "pyqt5": QT_API_PYQT5, "pyside2": QT_API_PYSIDE2, } # First, check if anything is already imported. if sys.modules.get("PyQt6.QtCore"): QT_API = QT_API_PYQT6 elif sys.modules.get("PySide6.QtCore"): QT_API = QT_API_PYSIDE6 elif sys.modules.get("PyQt5.QtCore"): QT_API = QT_API_PYQT5 elif sys.modules.get("PySide2.QtCore"): QT_API = QT_API_PYSIDE2 # 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). Use _get_backend_or_none to avoid # triggering backend resolution (which can result in a partially but # incompletely imported backend_qt5). elif (mpl.rcParams._get_backend_or_none() or "").lower().startswith("qt5"): if QT_API_ENV in ["pyqt5", "pyside2"]: QT_API = _ETS[QT_API_ENV] else: _QT_FORCE_QT5_BINDING = True # noqa 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). elif QT_API_ENV is None: QT_API = None elif QT_API_ENV in _ETS: QT_API = _ETS[QT_API_ENV] else: raise RuntimeError( "The environment variable QT_API has the unrecognized value {!r}; " "valid values are {}".format(QT_API_ENV, ", ".join(_ETS))) def _setup_pyqt5plus(): global QtCore, QtGui, QtWidgets, __version__ global _isdeleted, _to_int if QT_API == QT_API_PYQT6: from PyQt6 import QtCore, QtGui, QtWidgets, sip __version__ = QtCore.PYQT_VERSION_STR QtCore.Signal = QtCore.pyqtSignal QtCore.Slot = QtCore.pyqtSlot QtCore.Property = QtCore.pyqtProperty _isdeleted = sip.isdeleted _to_int = operator.attrgetter('value') elif QT_API == QT_API_PYSIDE6: from PySide6 import QtCore, QtGui, QtWidgets, __version__ import shiboken6 def _isdeleted(obj): return not shiboken6.isValid(obj) if parse_version(__version__) >= parse_version('6.4'): _to_int = operator.attrgetter('value') else: _to_int = int elif 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 _to_int = int elif QT_API == QT_API_PYSIDE2: from PySide2 import QtCore, QtGui, QtWidgets, __version__ try: from PySide2 import shiboken2 except ImportError: import shiboken2 def _isdeleted(obj): return not shiboken2.isValid(obj) _to_int = int else: raise AssertionError(f"Unexpected QT_API: {QT_API}") if QT_API in [QT_API_PYQT6, QT_API_PYQT5, QT_API_PYSIDE6, QT_API_PYSIDE2]: _setup_pyqt5plus() elif QT_API is None: # See above re: dict.__getitem__. if _QT_FORCE_QT5_BINDING: _candidates = [ (_setup_pyqt5plus, QT_API_PYQT5), (_setup_pyqt5plus, QT_API_PYSIDE2), ] else: _candidates = [ (_setup_pyqt5plus, QT_API_PYQT6), (_setup_pyqt5plus, QT_API_PYSIDE6), (_setup_pyqt5plus, QT_API_PYQT5), (_setup_pyqt5plus, QT_API_PYSIDE2), ] for _setup, QT_API in _candidates: try: _setup() except ImportError: continue break else: raise ImportError( "Failed to import any of the following Qt binding modules: {}" .format(", ".join([QT_API for _, QT_API in _candidates])) ) else: # We should not get there. raise AssertionError(f"Unexpected QT_API: {QT_API}") _version_info = tuple(QtCore.QLibraryInfo.version().segments()) if _version_info < (5, 12): raise ImportError( f"The Qt version imported is " f"{QtCore.QLibraryInfo.version().toString()} but Matplotlib requires " f"Qt>=5.12") # Fixes issues with Big Sur # https://bugreports.qt.io/browse/QTBUG-87014, fixed in qt 5.15.2 if (sys.platform == 'darwin' and parse_version(platform.mac_ver()[0]) >= parse_version("10.16") and _version_info < (5, 15, 2)): os.environ.setdefault("QT_MAC_WANTS_LAYER", "1") # Backports. def _exec(obj): # exec on PyQt6, exec_ elsewhere. obj.exec() if hasattr(obj, "exec") else obj.exec_()