diff --git a/doc/api/next_api_changes/development/25363-OG.rst b/doc/api/next_api_changes/development/25363-OG.rst new file mode 100644 index 000000000000..c73696d3472c --- /dev/null +++ b/doc/api/next_api_changes/development/25363-OG.rst @@ -0,0 +1,4 @@ +Support for Qt<5.12 has been dropped +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... as there are no wheels or conda packages that support both Qt 5.11 (or +older) and Python 3.9 (or newer). diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index f4b281bdd05c..5e02ed942d31 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -14,14 +14,14 @@ from . import qt_compat from .qt_compat import ( QtCore, QtGui, QtWidgets, __version__, QT_API, - _enum, _to_int, _isdeleted, _maybe_allow_interrupt + _to_int, _isdeleted, _maybe_allow_interrupt ) # SPECIAL_KEYS are Qt::Key that do *not* return their Unicode name # instead they have manually specified names. SPECIAL_KEYS = { - _to_int(getattr(_enum("QtCore.Qt.Key"), k)): v for k, v in [ + _to_int(getattr(QtCore.Qt.Key, k)): v for k, v in [ ("Key_Escape", "escape"), ("Key_Tab", "tab"), ("Key_Backspace", "backspace"), @@ -66,8 +66,8 @@ # Elements are (Qt::KeyboardModifiers, Qt::Key) tuples. # Order determines the modifier order (ctrl+alt+...) reported by Matplotlib. _MODIFIER_KEYS = [ - (_to_int(getattr(_enum("QtCore.Qt.KeyboardModifier"), mod)), - _to_int(getattr(_enum("QtCore.Qt.Key"), key))) + (_to_int(getattr(QtCore.Qt.KeyboardModifier, mod)), + _to_int(getattr(QtCore.Qt.Key, key))) for mod, key in [ ("ControlModifier", "Key_Control"), ("AltModifier", "Key_Alt"), @@ -76,7 +76,7 @@ ] ] cursord = { - k: getattr(_enum("QtCore.Qt.CursorShape"), v) for k, v in [ + k: getattr(QtCore.Qt.CursorShape, v) for k, v in [ (cursors.MOVE, "SizeAllCursor"), (cursors.HAND, "PointingHandCursor"), (cursors.POINTER, "ArrowCursor"), @@ -142,7 +142,6 @@ def _create_qApp(): app.setWindowIcon(icon) app.lastWindowClosed.connect(app.quit) cbook._setup_new_guiapp() - if qt_version == 5: app.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps) @@ -184,7 +183,7 @@ class FigureCanvasQT(FigureCanvasBase, QtWidgets.QWidget): manager_class = _api.classproperty(lambda cls: FigureManagerQT) buttond = { - getattr(_enum("QtCore.Qt.MouseButton"), k): v for k, v in [ + getattr(QtCore.Qt.MouseButton, k): v for k, v in [ ("LeftButton", MouseButton.LEFT), ("RightButton", MouseButton.RIGHT), ("MiddleButton", MouseButton.MIDDLE), @@ -202,8 +201,7 @@ def __init__(self, figure=None): self._draw_rect_callback = lambda painter: None self._in_resize_event = False - self.setAttribute( - _enum("QtCore.Qt.WidgetAttribute").WA_OpaquePaintEvent) + self.setAttribute(QtCore.Qt.WidgetAttribute.WA_OpaquePaintEvent) self.setMouseTracking(True) self.resize(*self.get_width_height()) @@ -554,7 +552,7 @@ def __init__(self, canvas, num): # StrongFocus accepts both tab and click to focus and will enable the # canvas to process event without clicking. # https://doc.qt.io/qt-5/qt.html#FocusPolicy-enum - self.canvas.setFocusPolicy(_enum("QtCore.Qt.FocusPolicy").StrongFocus) + self.canvas.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus) self.canvas.setFocus() self.window.raise_() @@ -634,9 +632,8 @@ def __init__(self, canvas, parent=None, coordinates=True): """coordinates: should we show the coordinates on the right?""" QtWidgets.QToolBar.__init__(self, parent) self.setAllowedAreas(QtCore.Qt.ToolBarArea( - _to_int(_enum("QtCore.Qt.ToolBarArea").TopToolBarArea) | - _to_int(_enum("QtCore.Qt.ToolBarArea").BottomToolBarArea))) - + _to_int(QtCore.Qt.ToolBarArea.TopToolBarArea) | + _to_int(QtCore.Qt.ToolBarArea.BottomToolBarArea))) self.coordinates = coordinates self._actions = {} # mapping of toolitem method names to QActions. self._subplot_dialog = None @@ -659,11 +656,12 @@ def __init__(self, canvas, parent=None, coordinates=True): if self.coordinates: self.locLabel = QtWidgets.QLabel("", self) self.locLabel.setAlignment(QtCore.Qt.AlignmentFlag( - _to_int(_enum("QtCore.Qt.AlignmentFlag").AlignRight) | - _to_int(_enum("QtCore.Qt.AlignmentFlag").AlignVCenter))) + _to_int(QtCore.Qt.AlignmentFlag.AlignRight) | + _to_int(QtCore.Qt.AlignmentFlag.AlignVCenter))) + self.locLabel.setSizePolicy(QtWidgets.QSizePolicy( - _enum("QtWidgets.QSizePolicy.Policy").Expanding, - _enum("QtWidgets.QSizePolicy.Policy").Ignored, + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Ignored, )) labelAction = self.addWidget(self.locLabel) labelAction.setVisible(True) @@ -689,7 +687,7 @@ def _icon(self, name): icon_color = self.palette().color(self.foregroundRole()) mask = pm.createMaskFromColor( QtGui.QColor('black'), - _enum("QtCore.Qt.MaskMode").MaskOutColor) + QtCore.Qt.MaskMode.MaskOutColor) pm.fill(icon_color) pm.setMask(mask) return QtGui.QIcon(pm) @@ -793,8 +791,8 @@ def save_figure(self, *args): except Exception as e: QtWidgets.QMessageBox.critical( self, "Error saving file", str(e), - _enum("QtWidgets.QMessageBox.StandardButton").Ok, - _enum("QtWidgets.QMessageBox.StandardButton").NoButton) + QtWidgets.QMessageBox.StandardButton.Ok, + QtWidgets.QMessageBox.StandardButton.NoButton) def set_history_buttons(self): can_backward = self._nav_stack._pos > 0 @@ -908,15 +906,15 @@ def __init__(self, toolmanager, parent=None): ToolContainerBase.__init__(self, toolmanager) QtWidgets.QToolBar.__init__(self, parent) self.setAllowedAreas(QtCore.Qt.ToolBarArea( - _to_int(_enum("QtCore.Qt.ToolBarArea").TopToolBarArea) | - _to_int(_enum("QtCore.Qt.ToolBarArea").BottomToolBarArea))) + _to_int(QtCore.Qt.ToolBarArea.TopToolBarArea) | + _to_int(QtCore.Qt.ToolBarArea.BottomToolBarArea))) message_label = QtWidgets.QLabel("") message_label.setAlignment(QtCore.Qt.AlignmentFlag( - _to_int(_enum("QtCore.Qt.AlignmentFlag").AlignRight) | - _to_int(_enum("QtCore.Qt.AlignmentFlag").AlignVCenter))) + _to_int(QtCore.Qt.AlignmentFlag.AlignRight) | + _to_int(QtCore.Qt.AlignmentFlag.AlignVCenter))) message_label.setSizePolicy(QtWidgets.QSizePolicy( - _enum("QtWidgets.QSizePolicy.Policy").Expanding, - _enum("QtWidgets.QSizePolicy.Policy").Ignored, + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Ignored, )) self._message_action = self.addWidget(message_label) self._toolitems = {} diff --git a/lib/matplotlib/backends/backend_qtagg.py b/lib/matplotlib/backends/backend_qtagg.py index f89de8efba97..d08575d284a2 100644 --- a/lib/matplotlib/backends/backend_qtagg.py +++ b/lib/matplotlib/backends/backend_qtagg.py @@ -6,9 +6,9 @@ from matplotlib.transforms import Bbox -from .qt_compat import QT_API, _enum +from .qt_compat import QT_API, QtCore, QtGui from .backend_agg import FigureCanvasAgg -from .backend_qt import QtCore, QtGui, _BackendQT, FigureCanvasQT +from .backend_qt import _BackendQT, FigureCanvasQT from .backend_qt import ( # noqa: F401 # pylint: disable=W0611 FigureManagerQT, NavigationToolbar2QT) @@ -57,7 +57,7 @@ def paintEvent(self, event): painter.eraseRect(rect) # clear the widget canvas qimage = QtGui.QImage(ptr, buf.shape[1], buf.shape[0], - _enum("QtGui.QImage.Format").Format_RGBA8888) + QtGui.QImage.Format.Format_RGBA8888) qimage.setDevicePixelRatio(self.device_pixel_ratio) # set origin using original QT coordinates origin = QtCore.QPoint(rect.left(), rect.top()) diff --git a/lib/matplotlib/backends/backend_qtcairo.py b/lib/matplotlib/backends/backend_qtcairo.py index cca1be012f9e..72eb2dc70b90 100644 --- a/lib/matplotlib/backends/backend_qtcairo.py +++ b/lib/matplotlib/backends/backend_qtcairo.py @@ -1,8 +1,8 @@ import ctypes from .backend_cairo import cairo, FigureCanvasCairo -from .backend_qt import QtCore, QtGui, _BackendQT, FigureCanvasQT -from .qt_compat import QT_API, _enum +from .backend_qt import _BackendQT, FigureCanvasQT +from .qt_compat import QT_API, QtCore, QtGui class FigureCanvasQTCairo(FigureCanvasCairo, FigureCanvasQT): @@ -28,7 +28,7 @@ def paintEvent(self, event): ptr = buf qimage = QtGui.QImage( ptr, width, height, - _enum("QtGui.QImage.Format").Format_ARGB32_Premultiplied) + QtGui.QImage.Format.Format_ARGB32_Premultiplied) # Adjust the buf reference count to work around a memory leak bug in # QImage under PySide. if QT_API == "PySide2" and QtCore.__version_info__ < (5, 12): diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index e76ff67e1cef..bd2aa0d2c968 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -9,7 +9,6 @@ - otherwise, use whatever the rcParams indicate. """ -import functools import operator import os import platform @@ -140,11 +139,11 @@ def _isdeleted(obj): _version_info = tuple(QtCore.QLibraryInfo.version().segments()) -if _version_info < (5, 10): +if _version_info < (5, 12): raise ImportError( f"The Qt version imported is " f"{QtCore.QLibraryInfo.version().toString()} but Matplotlib requires " - f"Qt>=5.10") + f"Qt>=5.12") # Fixes issues with Big Sur @@ -155,17 +154,6 @@ def _isdeleted(obj): os.environ.setdefault("QT_MAC_WANTS_LAYER", "1") -# PyQt6 enum compat helpers. - - -@functools.cache -def _enum(name): - # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6). - return operator.attrgetter( - name if QT_API == 'PyQt6' else name.rpartition(".")[0] - )(sys.modules[QtCore.__package__]) - - # Backports. @@ -208,7 +196,7 @@ def _maybe_allow_interrupt(qapp): wsock.setblocking(False) old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno()) sn = QtCore.QSocketNotifier( - rsock.fileno(), _enum('QtCore.QSocketNotifier.Type').Read + rsock.fileno(), QtCore.QSocketNotifier.Type.Read ) # We do not actually care about this value other than running some diff --git a/lib/matplotlib/backends/qt_editor/_formlayout.py b/lib/matplotlib/backends/qt_editor/_formlayout.py index 0f493f76b088..fcf73cefbac8 100644 --- a/lib/matplotlib/backends/qt_editor/_formlayout.py +++ b/lib/matplotlib/backends/qt_editor/_formlayout.py @@ -49,8 +49,7 @@ from numbers import Integral, Real from matplotlib import _api, colors as mcolors -from matplotlib.backends.qt_compat import ( - QtGui, QtWidgets, QtCore, _enum, _to_int) +from matplotlib.backends.qt_compat import _to_int, QtGui, QtWidgets, QtCore _log = logging.getLogger(__name__) @@ -73,7 +72,7 @@ def __init__(self, parent=None): def choose_color(self): color = QtWidgets.QColorDialog.getColor( self._color, self.parentWidget(), "", - _enum("QtWidgets.QColorDialog.ColorDialogOption").ShowAlphaChannel) + QtWidgets.QColorDialog.ColorDialogOption.ShowAlphaChannel) if color.isValid(): self.set_color(color) @@ -206,7 +205,7 @@ def get_font(self): def is_edit_valid(edit): text = edit.text() state = edit.validator().validate(text, 0)[0] - return state == _enum("QtGui.QDoubleValidator.State").Acceptable + return state == QtGui.QDoubleValidator.State.Acceptable class FormWidget(QtWidgets.QWidget): @@ -444,15 +443,13 @@ def __init__(self, data, title="", comment="", # Button box self.bbox = bbox = QtWidgets.QDialogButtonBox( QtWidgets.QDialogButtonBox.StandardButton( - _to_int( - _enum("QtWidgets.QDialogButtonBox.StandardButton").Ok) | - _to_int( - _enum("QtWidgets.QDialogButtonBox.StandardButton").Cancel) + _to_int(QtWidgets.QDialogButtonBox.StandardButton.Ok) | + _to_int(QtWidgets.QDialogButtonBox.StandardButton.Cancel) )) self.formwidget.update_buttons.connect(self.update_buttons) if self.apply_callback is not None: apply_btn = bbox.addButton( - _enum("QtWidgets.QDialogButtonBox.StandardButton").Apply) + QtWidgets.QDialogButtonBox.StandardButton.Apply) apply_btn.clicked.connect(self.apply) bbox.accepted.connect(self.accept) @@ -477,7 +474,7 @@ def update_buttons(self): valid = False for btn_type in ["Ok", "Apply"]: btn = self.bbox.button( - getattr(_enum("QtWidgets.QDialogButtonBox.StandardButton"), + getattr(QtWidgets.QDialogButtonBox.StandardButton, btn_type)) if btn is not None: btn.setEnabled(valid) diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index e24eac7a5292..8d7f239e23b1 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -307,20 +307,20 @@ def test_correct_key(backend, qt_core, qt_key, qt_mods, answer, monkeypatch): Catch the event. Assert sent and caught keys are the same. """ - from matplotlib.backends.qt_compat import _enum, _to_int + from matplotlib.backends.qt_compat import _to_int, QtCore if sys.platform == "darwin" and answer is not None: answer = answer.replace("ctrl", "cmd") answer = answer.replace("control", "cmd") answer = answer.replace("meta", "ctrl") result = None - qt_mod = _enum("QtCore.Qt.KeyboardModifier").NoModifier + qt_mod = QtCore.Qt.KeyboardModifier.NoModifier for mod in qt_mods: - qt_mod |= getattr(_enum("QtCore.Qt.KeyboardModifier"), mod) + qt_mod |= getattr(QtCore.Qt.KeyboardModifier, mod) class _Event: def isAutoRepeat(self): return False - def key(self): return _to_int(getattr(_enum("QtCore.Qt.Key"), qt_key)) + def key(self): return _to_int(getattr(QtCore.Qt.Key, qt_key)) monkeypatch.setattr(QtWidgets.QApplication, "keyboardModifiers", lambda self: qt_mod) @@ -494,123 +494,6 @@ def test_form_widget_get_with_datetime_and_date_fields(): ] -# The source of this function gets extracted and run in another process, so it -# must be fully self-contained. -def _test_enums_impl(): - import sys - - from matplotlib.backends.qt_compat import _enum, _to_int - from matplotlib.backend_bases import cursors, MouseButton - - _enum("QtGui.QDoubleValidator.State").Acceptable - - _enum("QtWidgets.QDialogButtonBox.StandardButton").Ok - _enum("QtWidgets.QDialogButtonBox.StandardButton").Cancel - _enum("QtWidgets.QDialogButtonBox.StandardButton").Apply - for btn_type in ["Ok", "Cancel"]: - getattr(_enum("QtWidgets.QDialogButtonBox.StandardButton"), btn_type) - - _enum("QtGui.QImage.Format").Format_ARGB32_Premultiplied - _enum("QtGui.QImage.Format").Format_ARGB32_Premultiplied - # SPECIAL_KEYS are Qt::Key that do *not* return their Unicode name instead - # they have manually specified names. - SPECIAL_KEYS = { - _to_int(getattr(_enum("QtCore.Qt.Key"), k)): v - for k, v in [ - ("Key_Escape", "escape"), - ("Key_Tab", "tab"), - ("Key_Backspace", "backspace"), - ("Key_Return", "enter"), - ("Key_Enter", "enter"), - ("Key_Insert", "insert"), - ("Key_Delete", "delete"), - ("Key_Pause", "pause"), - ("Key_SysReq", "sysreq"), - ("Key_Clear", "clear"), - ("Key_Home", "home"), - ("Key_End", "end"), - ("Key_Left", "left"), - ("Key_Up", "up"), - ("Key_Right", "right"), - ("Key_Down", "down"), - ("Key_PageUp", "pageup"), - ("Key_PageDown", "pagedown"), - ("Key_Shift", "shift"), - # In OSX, the control and super (aka cmd/apple) keys are switched. - ("Key_Control", "control" if sys.platform != "darwin" else "cmd"), - ("Key_Meta", "meta" if sys.platform != "darwin" else "control"), - ("Key_Alt", "alt"), - ("Key_CapsLock", "caps_lock"), - ("Key_F1", "f1"), - ("Key_F2", "f2"), - ("Key_F3", "f3"), - ("Key_F4", "f4"), - ("Key_F5", "f5"), - ("Key_F6", "f6"), - ("Key_F7", "f7"), - ("Key_F8", "f8"), - ("Key_F9", "f9"), - ("Key_F10", "f10"), - ("Key_F10", "f11"), - ("Key_F12", "f12"), - ("Key_Super_L", "super"), - ("Key_Super_R", "super"), - ] - } - # Define which modifier keys are collected on keyboard events. Elements - # are (Qt::KeyboardModifiers, Qt::Key) tuples. Order determines the - # modifier order (ctrl+alt+...) reported by Matplotlib. - _MODIFIER_KEYS = [ - ( - _to_int(getattr(_enum("QtCore.Qt.KeyboardModifier"), mod)), - _to_int(getattr(_enum("QtCore.Qt.Key"), key)), - ) - for mod, key in [ - ("ControlModifier", "Key_Control"), - ("AltModifier", "Key_Alt"), - ("ShiftModifier", "Key_Shift"), - ("MetaModifier", "Key_Meta"), - ] - ] - cursord = { - k: getattr(_enum("QtCore.Qt.CursorShape"), v) - for k, v in [ - (cursors.MOVE, "SizeAllCursor"), - (cursors.HAND, "PointingHandCursor"), - (cursors.POINTER, "ArrowCursor"), - (cursors.SELECT_REGION, "CrossCursor"), - (cursors.WAIT, "WaitCursor"), - ] - } - - buttond = { - getattr(_enum("QtCore.Qt.MouseButton"), k): v - for k, v in [ - ("LeftButton", MouseButton.LEFT), - ("RightButton", MouseButton.RIGHT), - ("MiddleButton", MouseButton.MIDDLE), - ("XButton1", MouseButton.BACK), - ("XButton2", MouseButton.FORWARD), - ] - } - - _enum("QtCore.Qt.WidgetAttribute").WA_OpaquePaintEvent - _enum("QtCore.Qt.FocusPolicy").StrongFocus - _enum("QtCore.Qt.ToolBarArea").TopToolBarArea - _enum("QtCore.Qt.ToolBarArea").TopToolBarArea - _enum("QtCore.Qt.AlignmentFlag").AlignRight - _enum("QtCore.Qt.AlignmentFlag").AlignVCenter - _enum("QtWidgets.QSizePolicy.Policy").Expanding - _enum("QtWidgets.QSizePolicy.Policy").Ignored - _enum("QtCore.Qt.MaskMode").MaskOutColor - _enum("QtCore.Qt.ToolBarArea").TopToolBarArea - _enum("QtCore.Qt.ToolBarArea").TopToolBarArea - _enum("QtCore.Qt.AlignmentFlag").AlignRight - _enum("QtCore.Qt.AlignmentFlag").AlignVCenter - _enum("QtWidgets.QSizePolicy.Policy").Expanding - _enum("QtWidgets.QSizePolicy.Policy").Ignored - - def _get_testable_qt_backends(): envs = [] for deps, env in [ @@ -632,13 +515,3 @@ def _get_testable_qt_backends(): reason=f"Skipping {env} because {reason}")) envs.append(pytest.param(env, marks=marks, id=str(env))) return envs - - -@pytest.mark.parametrize("env", _get_testable_qt_backends()) -def test_enums_available(env): - proc = subprocess.run( - [sys.executable, "-c", - inspect.getsource(_test_enums_impl) + "\n_test_enums_impl()"], - env={**os.environ, "SOURCE_DATE_EPOCH": "0", **env}, - timeout=_test_timeout, check=True, - stdout=subprocess.PIPE, text=True)