8000 Rewrite and greatly simplify qt_compat.py. · matplotlib/matplotlib@df8b780 · GitHub
[go: up one dir, main page]

Skip to content
  • Commit df8b780

    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 df8b780

    File tree

    4 files changed

    +120
    -199
    lines changed

    4 files changed

    +120
    -199
    lines changed

    INSTALL.rst

    Lines changed: 3 additions & 2 deletions
    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

    Lines changed: 3 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -355,6 +355,9 @@ class Frame(object):
    355355

    356356

    357357
    class MyPyQt4(MagicMock):
    358+
    class QtCore(object):
    359+
    PYQT_VERSION_STR = "4.6"
    360+
    358361
    class QtGui(object):
    359362
    # PyQt4.QtGui public classes.
    360363
    # Generated with

    lib/matplotlib/backends/qt_compat.py

    Lines changed: 113 additions & 196 deletions
    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+
    57A6 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)
    < F438 /td>
    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

    Lines changed: 1 addition & 1 deletion
    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)
    0