From b8071e032d0e87b6200c12ed2c803b6bee5733af Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 12 Dec 2017 22:55:10 -0800 Subject: [PATCH 1/2] 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. --- INSTALL.rst | 5 +- doc/api/backend_qt4agg_api.rst | 10 +- doc/api/backend_qt4cairo_api.rst | 10 +- doc/api/backend_qt5agg_api.rst | 10 +- doc/api/backend_qt5cairo_api.rst | 10 +- doc/sphinxext/mock_gui_toolkits.py | 104 --------- lib/matplotlib/backends/qt_compat.py | 333 +++++++++++---------------- 7 files changed, 163 insertions(+), 319 deletions(-) diff --git a/INSTALL.rst b/INSTALL.rst index 755e7e59e17a..d212b2b1fb7d 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -150,8 +150,9 @@ interface toolkits. See :ref:`what-is-a-backend` for more details on the optional Matplotlib backends and the capabilities they provide. * :term:`tk` (>= 8.3, != 8.6.0 or 8.6.1): for the Tk-based backends; -* `PyQt4 `_ (>= 4.4) or - `PySide `_: for the Qt4-based backends; +* `PyQt4 `_ (>= 4.6) or + `PySide `_ (>= 1.0.3): for the Qt4-based + backends; * `PyQt5 `_: for the Qt5-based backends; * `PyGObject `_ or `pgi `_ (>= 0.0.11.2): for the GTK3-based diff --git a/doc/api/backend_qt4agg_api.rst b/doc/api/backend_qt4agg_api.rst index 8bf490aa8cb9..8b787512a44c 100644 --- a/doc/api/backend_qt4agg_api.rst +++ b/doc/api/backend_qt4agg_api.rst @@ -2,7 +2,9 @@ :mod:`matplotlib.backends.backend_qt4agg` ========================================= -.. automodule:: matplotlib.backends.backend_qt4agg - :members: - :undoc-members: - :show-inheritance: +**NOTE** Not included, to avoid adding a dependency to building the docs. + +.. .. automodule:: matplotlib.backends.backend_qt4agg +.. :members: +.. :undoc-members: +.. :show-inheritance: diff --git a/doc/api/backend_qt4cairo_api.rst b/doc/api/backend_qt4cairo_api.rst index 590465d7fbc0..1e6cb526de96 100644 --- a/doc/api/backend_qt4cairo_api.rst +++ b/doc/api/backend_qt4cairo_api.rst @@ -2,7 +2,9 @@ :mod:`matplotlib.backends.backend_qt4cairo` =========================================== -.. automodule:: matplotlib.backends.backend_qt4cairo - :members: - :undoc-members: - :show-inheritance: +**NOTE** Not included, to avoid adding a dependency to building the docs. + +.. .. automodule:: matplotlib.backends.backend_qt4cairo +.. :members: +.. :undoc-members: +.. :show-inheritance: diff --git a/doc/api/backend_qt5agg_api.rst b/doc/api/backend_qt5agg_api.rst index 8d1ad2aba0f0..f8400aefa1a2 100644 --- a/doc/api/backend_qt5agg_api.rst +++ b/doc/api/backend_qt5agg_api.rst @@ -2,7 +2,9 @@ :mod:`matplotlib.backends.backend_qt5agg` ========================================= -.. automodule:: matplotlib.backends.backend_qt5agg - :members: - :undoc-members: - :show-inheritance: +**NOTE** Not included, to avoid adding a dependency to building the docs. + +.. .. automodule:: matplotlib.backends.backend_qt5agg +.. :members: +.. :undoc-members: +.. :show-inheritance: diff --git a/doc/api/backend_qt5cairo_api.rst b/doc/api/backend_qt5cairo_api.rst index 73df7ac128a1..7ff3e1233b43 100644 --- a/doc/api/backend_qt5cairo_api.rst +++ b/doc/api/backend_qt5cairo_api.rst @@ -2,7 +2,9 @@ :mod:`matplotlib.backends.backend_qt5cairo` =========================================== -.. automodule:: matplotlib.backends.backend_qt5cairo - :members: - :undoc-members: - :show-inheritance: +**NOTE** Not included, to avoid adding a dependency to building the docs. + +.. .. automodule:: matplotlib.backends.backend_qt5cairo +.. :members: +.. :undoc-members: +.. :show-inheritance: diff --git a/doc/sphinxext/mock_gui_toolkits.py b/doc/sphinxext/mock_gui_toolkits.py index 8e8cc071ff70..a7be59a940c5 100644 --- a/doc/sphinxext/mock_gui_toolkits.py +++ b/doc/sphinxext/mock_gui_toolkits.py @@ -6,108 +6,6 @@ class MyCairoCffi(MagicMock): __name__ = "cairocffi" -class MyPyQt4(MagicMock): - class QtGui(object): - # PyQt4.QtGui public classes. - # Generated with - # textwrap.fill([name for name in dir(PyQt4.QtGui) - # if isinstance(getattr(PyQt4.QtGui, name), type)]) - _QtGui_public_classes = """\ - Display QAbstractButton QAbstractGraphicsShapeItem - QAbstractItemDelegate QAbstractItemView QAbstractPrintDialog - QAbstractProxyModel QAbstractScrollArea QAbstractSlider - QAbstractSpinBox QAbstractTextDocumentLayout QAction QActionEvent - QActionGroup QApplication QBitmap QBoxLayout QBrush QButtonGroup - QCalendarWidget QCheckBox QClipboard QCloseEvent QColor QColorDialog - QColumnView QComboBox QCommandLinkButton QCommonStyle QCompleter - QConicalGradient QContextMenuEvent QCursor QDataWidgetMapper QDateEdit - QDateTimeEdit QDesktopServices QDesktopWidget QDial QDialog - QDialogButtonBox QDirModel QDockWidget QDoubleSpinBox QDoubleValidator - QDrag QDragEnterEvent QDragLeaveEvent QDragMoveEvent QDropEvent - QErrorMessage QFileDialog QFileIconProvider QFileOpenEvent - QFileSystemModel QFocusEvent QFocusFrame QFont QFontComboBox - QFontDatabase QFontDialog QFontInfo QFontMetrics QFontMetricsF - QFormLayout QFrame QGesture QGestureEvent QGestureRecognizer QGlyphRun - QGradient QGraphicsAnchor QGraphicsAnchorLayout QGraphicsBlurEffect - QGraphicsColorizeEffect QGraphicsDropShadowEffect QGraphicsEffect - QGraphicsEllipseItem QGraphicsGridLayout QGraphicsItem - QGraphicsItemAnimation QGraphicsItemGroup QGraphicsLayout - QGraphicsLayoutItem QGraphicsLineItem QGraphicsLinearLayout - QGraphicsObject QGraphicsOpacityEffect QGraphicsPathItem - QGraphicsPixmapItem QGraphicsPolygonItem QGraphicsProxyWidget - QGraphicsRectItem QGraphicsRotation QGraphicsScale QGraphicsScene - QGraphicsSceneContextMenuEvent QGraphicsSceneDragDropEvent - QGraphicsSceneEvent QGraphicsSceneHelpEvent QGraphicsSceneHoverEvent - QGraphicsSceneMouseEvent QGraphicsSceneMoveEvent - QGraphicsSceneResizeEvent QGraphicsSceneWheelEvent - QGraphicsSimpleTextItem QGraphicsTextItem QGraphicsTransform - QGraphicsView QGraphicsWidget QGridLayout QGroupBox QHBoxLayout - QHeaderView QHelpEvent QHideEvent QHoverEvent QIcon QIconDragEvent - QIconEngine QIconEngineV2 QIdentityProxyModel QImage QImageIOHandler - QImageReader QImageWriter QInputContext QInputContextFactory - QInputDialog QInputEvent QInputMethodEvent QIntValidator QItemDelegate - QItemEditorCreatorBase QItemEditorFactory QItemSelection - QItemSelectionModel QItemSelectionRange QKeyEvent QKeyEventTransition - QKeySequence QLCDNumber QLabel QLayout QLayoutItem QLineEdit - QLinearGradient QListView QListWidget QListWidgetItem QMainWindow - QMatrix QMatrix2x2 QMatrix2x3 QMatrix2x4 QMatrix3x2 QMatrix3x3 - QMatrix3x4 QMatrix4x2 QMatrix4x3 QMatrix4x4 QMdiArea QMdiSubWindow - QMenu QMenuBar QMessageBox QMimeSource QMouseEvent - QMouseEventTransition QMoveEvent QMovie QPageSetupDialog QPaintDevice - QPaintEngine QPaintEngineState QPaintEvent QPainter QPainterPath - QPainterPathStroker QPalette QPanGesture QPen QPicture QPictureIO - QPinchGesture QPixmap QPixmapCache QPlainTextDocumentLayout - QPlainTextEdit QPolygon QPolygonF QPrintDialog QPrintEngine - QPrintPreviewDialog QPrintPreviewWidget QPrinter QPrinterInfo - QProgressBar QProgressDialog QProxyModel QPushButton QPyTextObject - QQuaternion QRadialGradient QRadioButton QRawFont QRegExpValidator - QRegion QResizeEvent QRubberBand QScrollArea QScrollBar - QSessionManager QShortcut QShortcutEvent QShowEvent QSizeGrip - QSizePolicy QSlider QSortFilterProxyModel QSound QSpacerItem QSpinBox - QSplashScreen QSplitter QSplitterHandle QStackedLayout QStackedWidget - QStandardItem QStandardItemModel QStaticText QStatusBar - QStatusTipEvent QStringListModel QStyle QStyleFactory QStyleHintReturn - QStyleHintReturnMask QStyleHintReturnVariant QStyleOption - QStyleOptionButton QStyleOptionComboBox QStyleOptionComplex - QStyleOptionDockWidget QStyleOptionDockWidgetV2 QStyleOptionFocusRect - QStyleOptionFrame QStyleOptionFrameV2 QStyleOptionFrameV3 - QStyleOptionGraphicsItem QStyleOptionGroupBox QStyleOptionHeader - QStyleOptionMenuItem QStyleOptionProgressBar QStyleOptionProgressBarV2 - QStyleOptionRubberBand QStyleOptionSizeGrip QStyleOptionSlider - QStyleOptionSpinBox QStyleOptionTab QStyleOptionTabBarBase - QStyleOptionTabBarBaseV2 QStyleOptionTabV2 QStyleOptionTabV3 - QStyleOptionTabWidgetFrame QStyleOptionTabWidgetFrameV2 - QStyleOptionTitleBar QStyleOptionToolBar QStyleOptionToolBox - QStyleOptionToolBoxV2 QStyleOptionToolButton QStyleOptionViewItem - QStyleOptionViewItemV2 QStyleOptionViewItemV3 QStyleOptionViewItemV4 - QStylePainter QStyledItemDelegate QSwipeGesture QSyntaxHighlighter - QSystemTrayIcon QTabBar QTabWidget QTableView QTableWidget - QTableWidgetItem QTableWidgetSelectionRange QTabletEvent - QTapAndHoldGesture QTapGesture QTextBlock QTextBlockFormat - QTextBlockGroup QTextBlockUserData QTextBrowser QTextCharFormat - QTextCursor QTextDocument QTextDocumentFragment QTextDocumentWriter - QTextEdit QTextFormat QTextFragment QTextFrame QTextFrameFormat - QTextImageFormat QTextInlineObject QTextItem QTextLayout QTextLength - QTextLine QTextList QTextListFormat QTextObject QTextObjectInterface - QTextOption QTextTable QTextTableCell QTextTableCellFormat - QTextTableFormat QTimeEdit QToolBar QToolBox QToolButton QToolTip - QTouchEvent QTransform QTreeView QTreeWidget QTreeWidgetItem - QTreeWidgetItemIterator QUndoCommand QUndoGroup QUndoStack QUndoView - QVBoxLayout QValidator QVector2D QVector3D QVector4D QWhatsThis - QWhatsThisClickedEvent QWheelEvent QWidget QWidgetAction QWidgetItem - QWindowStateChangeEvent QWizard QWizardPage QWorkspace - QX11EmbedContainer QX11EmbedWidget QX11Info - """ - for _name in _QtGui_public_classes.split(): - locals()[_name] = type(_name, (), {}) - del _name - - -class MySip(MagicMock): - def getapi(*args): - return 1 - - class MyWX(MagicMock): class Panel(object): pass @@ -125,8 +23,6 @@ class StatusBar(object): def setup(app): sys.modules.update( cairocffi=MyCairoCffi(), - PyQt4=MyPyQt4(), - sip=MySip(), wx=MyWX(), ) return {'parallel_read_safe': True, 'parallel_write_safe': True} diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index 759fe24a7a66..d9db99ab0378 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -1,220 +1,159 @@ -""" A Qt API selector that can be used to switch between PyQt and PySide. +""" +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 Qt4Agg backend is requested but QT_API + is set to "pyqt5", then actually use Qt4 with the binding specified by + ``rcParams["backend.qt4"]``; +- otherwise, use whatever the rcParams indicate. """ +from distutils.version import LooseVersion import os -import logging import sys -from matplotlib import rcParams - -_log = logging.getLogger(__name__) -# Available APIs. -QT_API_PYQT = 'PyQt4' # API is not set here; Python 2.x default is V 1 -QT_API_PYQTv2 = 'PyQt4v2' # forced to Version 2 API -QT_API_PYSIDE = 'PySide' # only supports Version 2 API -QT_API_PYQT5 = 'PyQt5' # use PyQt5 API; Version 2 with module shim -QT_API_PYSIDE2 = 'PySide2' # Version 2 API with module shim +from matplotlib import rcParams -ETS = dict(pyqt=(QT_API_PYQTv2, 4), pyside=(QT_API_PYSIDE, 4), - pyqt5=(QT_API_PYQT5, 5), pyside2=(QT_API_PYSIDE2, 5)) -# ETS is a dict of env variable to (QT_API, QT_MAJOR_VERSION) -# If the ETS QT_API environment variable is set, use it, but only -# if the variable is of the same major QT version. Note that -# ETS requires the version 2 of PyQt4, which is not the platform -# default for Python 2.x. +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') - -if rcParams['backend'] == 'Qt5Agg': - QT_RC_MAJOR_VERSION = 5 -elif rcParams['backend'] == 'Qt4Agg': - QT_RC_MAJOR_VERSION = 4 -else: - # A different backend was specified, but we still got here because a Qt - # related file was imported. This is allowed, so lets try and guess - # what we should be using. - if "PyQt4" in sys.modules or "PySide" in sys.modules: - # PyQt4 or PySide is actually used. - QT_RC_MAJOR_VERSION = 4 - else: - # This is a fallback: PyQt5 - QT_RC_MAJOR_VERSION = 5 - -QT_API = None - -# check if any binding is already imported, if so silently ignore the -# rcparams/ENV settings and use what ever is already imported. -if 'PySide' in sys.modules: - # user has imported PySide before importing mpl - QT_API = QT_API_PYSIDE - -if 'PySide2' in sys.modules: - # user has imported PySide before importing mpl +# First, check if anything is already imported. +if "PyQt5" in sys.modules: + QT_API = QT_API_PYQT5 + dict.__setitem__(rcParams, "backend.qt5", QT_API) +elif "PySide2" in sys.modules: QT_API = QT_API_PYSIDE2 + dict.__setitem__(rcParams, "backend.qt5", QT_API) +elif "PyQt4" in sys.modules: + QT_API = QT_API_PYQTv2 + dict.__setitem__(rcParams, "backend.qt4", QT_API) +elif "PySide" in sys.modules: + QT_API = QT_API_PYSIDE + dict.__setitem__(rcParams, "backend.qt4", QT_API) +# 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"] == "Qt5Agg": + if QT_API_ENV == "pyqt5": + dict.__setitem__(rcParams, "backend.qt5", QT_API_PYQT5) + elif QT_API_ENV == "pyside2": + dict.__setitem__(rcParams, "backend.qt5", QT_API_PYSIDE2) + QT_API = dict.__getitem__(rcParams, "backend.qt5") +elif rcParams["backend"] == "Qt4Agg": + if QT_API_ENV == "pyqt4": + dict.__setitem__(rcParams, "backend.qt4", QT_API_PYQTv2) + elif QT_API_ENV == "pyside": + dict.__setitem__(rcParams, "backend.qt4", QT_API_PYSIDE) + QT_API = dict.__getitem__(rcParams, "backend.qt4") +# 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: + QT_API = None -if 'PyQt4' in sys.modules: - # user has imported PyQt4 before importing mpl - # this case also handles the PyQt4v2 case as once sip is imported - # the API versions can not be changed so do not try - QT_API = QT_API_PYQT - -if 'PyQt5' in sys.modules: - # the user has imported PyQt5 before importing mpl - QT_API = QT_API_PYQT5 -if (QT_API_ENV is not None) and QT_API is None: - try: - QT_ENV_MAJOR_VERSION = ETS[QT_API_ENV][1] - except KeyError: - raise RuntimeError( - ('Unrecognized environment variable %r, valid values are:' - ' %r, %r, %r or %r' - % (QT_API_ENV, 'pyqt', 'pyside', 'pyqt5', 'pyside2'))) - if QT_ENV_MAJOR_VERSION == QT_RC_MAJOR_VERSION: - # Only if backend and env qt major version are - # compatible use the env variable. - QT_API = ETS[QT_API_ENV][0] - -_fallback_to_qt4 = False -if QT_API is None: - # No ETS environment or incompatible so use rcParams. - if rcParams['backend'] == 'Qt5Agg': - QT_API = QT_API_PYQT5 - elif rcParams['backend'] == 'Qt4Agg': - QT_API = QT_API_PYQT - else: - # A non-Qt backend was specified, no version of the Qt - # bindings is imported, but we still got here because a Qt - # related file was imported. This is allowed, fall back to Qt5 - # using which ever binding the rparams ask for. - _fallback_to_qt4 = True - QT_API = QT_API_PYQT5 - -# We will define an appropriate wrapper for the differing versions -# of file dialog. -_getSaveFileName = None - -# Flag to check if sip could be imported -_sip_imported = False - -# Now perform the imports. -if QT_API in (QT_API_PYQT, QT_API_PYQTv2): - try: - import sip - _sip_imported = True - except ImportError: - # Try using PySide - if QT_RC_MAJOR_VERSION == 5: - QT_API = QT_API_PYSIDE2 - else: - QT_API = QT_API_PYSIDE - cond = ("Could not import sip; falling back on PySide\n" - "in place of PyQt4 or PyQt5.\n") - _log.info(cond) - -if _sip_imported: - if QT_API == QT_API_PYQTv2: - if QT_API_ENV == 'pyqt': - cond = ("Found 'QT_API=pyqt' environment variable. " - "Setting PyQt4 API accordingly.\n") - else: - cond = "PyQt API v2 specified." - try: - sip.setapi('QString', 2) - except: - res = 'QString API v2 specification failed. Defaulting to v1.' - _log.info(cond + res) - # condition has now been reported, no need to repeat it: - cond = "" - try: - sip.setapi('QVariant', 2) - except: - res = 'QVariant API v2 specification failed. Defaulting to v1.' - _log.info(cond + res) +def _setup_pyqt5(): + global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, _getSaveFileName -if QT_API == QT_API_PYQT5: - try: + if QT_API == QT_API_PYQT5: from PyQt5 import QtCore, QtGui, QtWidgets - _getSaveFileName = QtWidgets.QFileDialog.getSaveFileName - except ImportError: - if _fallback_to_qt4: - # fell through, tried PyQt5, failed fall back to PyQt4 - QT_API = QT_API_PYQT - QT_RC_MAJOR_VERSION = 4 - else: - raise - -if _sip_imported: - # needs to be if so we can re-test the value of QT_API which may - # have been changed in the above if block - if QT_API in [QT_API_PYQT, QT_API_PYQTv2]: # PyQt4 API - from PyQt4 import QtCore, QtGui - - try: - if sip.getapi("QString") > 1: - # Use new getSaveFileNameAndFilter() - _getSaveFileName = QtGui.QFileDialog.getSaveFileNameAndFilter - else: - - # Use old getSaveFileName() - def _getSaveFileName(*args, **kwargs): - return (QtGui.QFileDialog.getSaveFileName(*args, **kwargs), - None) - - except (AttributeError, KeyError): - - # call to getapi() can fail in older versions of sip - def _getSaveFileName(*args, **kwargs): - return QtGui.QFileDialog.getSaveFileName(*args, **kwargs), None - - -if QT_API == QT_API_PYSIDE2: - try: + __version__ = QtCore.PYQT_VERSION_STR + QtCore.Signal = QtCore.pyqtSignal + QtCore.Slot = QtCore.pyqtSlot + QtCore.Property = QtCore.pyqtProperty + elif QT_API == QT_API_PYSIDE2: from PySide2 import QtCore, QtGui, QtWidgets, __version__ - _getSaveFileName = QtWidgets.QFileDialog.getSaveFileName - except ImportError: - # tried PySide2, failed, fall back to PySide - QT_RC_MAJOR_VERSION = 4 - QT_API = QT_API_PYSIDE - -if QT_API == QT_API_PYSIDE: # try importing pyside - try: - from PySide import QtCore, QtGui, __version__, __version_info__ - except ImportError: - raise ImportError( - "Matplotlib qt-based backends require an external PyQt4, PyQt5,\n" - "PySide or PySide2 package to be installed, but it was not found.") + else: + raise ValueError("Unexpected value for the 'backend.qt5' rcparam") + _getSaveFileName = QtWidgets.QFileDialog.getSaveFileName - if __version_info__ < (1, 0, 3): - raise ImportError( - "Matplotlib backend_qt4 and backend_qt4agg require PySide >=1.0.3") + def is_pyqt5(): + return True - _getSaveFileName = QtGui.QFileDialog.getSaveFileName +def _setup_pyqt4(): + global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, _getSaveFileName -if QT_API in (QT_API_PYQT, QT_API_PYQTv2, QT_API_PYQT5): - # Alias PyQt-specific functions for PySide compatibility. - QtCore.Signal = QtCore.pyqtSignal - try: + 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"] + import sip + for _sip_api in _sip_apis: + try: + sip.setapi(_sip_api, api) + except ValueError: + pass + from PyQt4 import QtCore, QtGui + __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 - except AttributeError: - # Not a perfect match but works in simple cases - QtCore.Slot = QtCore.pyqtSignature + QtCore.Property = QtCore.pyqtProperty + _getSaveFileName = QtGui.QFileDialog.getSaveFileNameAndFilter - QtCore.Property = QtCore.pyqtProperty - __version__ = QtCore.PYQT_VERSION_STR - -# Apply shim to Qt4 APIs to make them look like Qt5 -if QT_API in (QT_API_PYQT, QT_API_PYQTv2, QT_API_PYSIDE): - '''Import all used QtGui objects into QtWidgets - - Here I've opted to simple copy QtGui into QtWidgets as that - achieves the same result as copying over the objects, and will - continue to work if other objects are used. - - ''' + 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__ + # 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") + _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)) -def is_pyqt5(): - return QT_API == QT_API_PYQT5 + +# These globals are only defined for backcompatibilty 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 From 1a7d56df0312c33704be703e7ea4070e5d54cd91 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 30 Jul 2018 15:47:34 -0400 Subject: [PATCH 2/2] MNT: be more paranoid about importing sip --- lib/matplotlib/backends/qt_compat.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index d9db99ab0378..6a0b0db480b9 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -88,12 +88,16 @@ def _setup_pyqt4_internal(api): # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html _sip_apis = ["QDate", "QDateTime", "QString", "QTextStream", "QTime", "QUrl", "QVariant"] - import sip - for _sip_api in _sip_apis: - try: - sip.setapi(_sip_api, api) - except ValueError: - pass + 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 __version__ = QtCore.PYQT_VERSION_STR # PyQt 4.6 introduced getSaveFileNameAndFilter: