From 32935fd7614ecf528b5a40c88ff25c70c79c620a Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 19 Feb 2022 13:03:51 +0100 Subject: [PATCH 1/2] Deprecate backend_qt.qApp. This global has subtly different semantics from QtWidgets.QApplication.instance() (it is only updated when the private _create_qApp is called), and therefore seems not worth exposing. --- .../deprecations/22503-AL.rst | 3 + lib/matplotlib/backends/backend_qt.py | 122 +++++++++--------- 2 files changed, 66 insertions(+), 59 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/22503-AL.rst diff --git a/doc/api/next_api_changes/deprecations/22503-AL.rst b/doc/api/next_api_changes/deprecations/22503-AL.rst new file mode 100644 index 000000000000..fb5990e5c4f0 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/22503-AL.rst @@ -0,0 +1,3 @@ +``backend_qt.qApp`` +~~~~~~~~~~~~~~~~~~~ +... is deprecated. Use ``QtWidgets.QApplication.instance()`` instead. diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 76b4d74ef640..6e48362cba36 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -92,70 +92,73 @@ } -# make place holder -qApp = None +@_api.caching_module_getattr +class __getattr__: + qApp = _api.deprecated( + "3.6", alternative="QtWidgets.QApplication.instance()")( + property(lambda self: QtWidgets.QApplication.instance())) +# lru_cache keeps a reference to the QApplication instance, keeping it from +# being GC'd. +@functools.lru_cache(1) def _create_qApp(): - """ - Only one qApp can exist at a time, so check before creating one. - """ - global qApp - - if qApp is None: - app = QtWidgets.QApplication.instance() - if app is None: - # display_is_valid returns False only if on Linux and neither X11 - # nor Wayland display can be opened. - if not mpl._c_internal_utils.display_is_valid(): - raise RuntimeError('Invalid DISPLAY variable') - try: - QtWidgets.QApplication.setAttribute( - QtCore.Qt.AA_EnableHighDpiScaling) - except AttributeError: # Only for Qt>=5.6, <6. - pass - - # Check to make sure a QApplication from a different major version - # of Qt is not instantiated in the process - if QT_API in {'PyQt6', 'PySide6'}: - other_bindings = ('PyQt5', 'PySide2') - elif QT_API in {'PyQt5', 'PySide2'}: - other_bindings = ('PyQt6', 'PySide6') - else: - raise RuntimeError("Should never be here") - - for binding in other_bindings: - mod = sys.modules.get(f'{binding}.QtWidgets') - if mod is not None and mod.QApplication.instance() is not None: - other_core = sys.modules.get(f'{binding}.QtCore') - _api.warn_external( - f'Matplotlib is using {QT_API} which wraps ' - f'{QtCore.qVersion()} however an instantiated ' - f'QApplication from {binding} which wraps ' - f'{other_core.qVersion()} exists. Mixing Qt major ' - 'versions may not work as expected.' - ) - break - try: - QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy( - QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) - except AttributeError: # Only for Qt>=5.14. - pass - qApp = QtWidgets.QApplication(["matplotlib"]) - if sys.platform == "darwin": - image = str(cbook._get_data_path('images/matplotlib.svg')) - icon = QtGui.QIcon(image) - qApp.setWindowIcon(icon) - qApp.lastWindowClosed.connect(qApp.quit) - cbook._setup_new_guiapp() + app = QtWidgets.QApplication.instance() + + # Create a new QApplication and configure if if non exists yet, as only one + # QApplication can exist at a time. + if app is None: + # display_is_valid returns False only if on Linux and neither X11 + # nor Wayland display can be opened. + if not mpl._c_internal_utils.display_is_valid(): + raise RuntimeError('Invalid DISPLAY variable') + + # Check to make sure a QApplication from a different major version + # of Qt is not instantiated in the process + if QT_API in {'PyQt6', 'PySide6'}: + other_bindings = ('PyQt5', 'PySide2') + elif QT_API in {'PyQt5', 'PySide2'}: + other_bindings = ('PyQt6', 'PySide6') else: - qApp = app + raise RuntimeError("Should never be here") + + for binding in other_bindings: + mod = sys.modules.get(f'{binding}.QtWidgets') + if mod is not None and mod.QApplication.instance() is not None: + other_core = sys.modules.get(f'{binding}.QtCore') + _api.warn_external( + f'Matplotlib is using {QT_API} which wraps ' + f'{QtCore.qVersion()} however an instantiated ' + f'QApplication from {binding} which wraps ' + f'{other_core.qVersion()} exists. Mixing Qt major ' + 'versions may not work as expected.' + ) + break + try: + QtWidgets.QApplication.setAttribute( + QtCore.Qt.AA_EnableHighDpiScaling) + except AttributeError: # Only for Qt>=5.6, <6. + pass + try: + QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy( + QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) + except AttributeError: # Only for Qt>=5.14. + pass + app = QtWidgets.QApplication(["matplotlib"]) + if sys.platform == "darwin": + image = str(cbook._get_data_path('images/matplotlib.svg')) + icon = QtGui.QIcon(image) + app.setWindowIcon(icon) + app.lastWindowClosed.connect(app.quit) + cbook._setup_new_guiapp() try: - qApp.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps) # Only for Qt<6. + app.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps) # Only for Qt<6. except AttributeError: pass + return app + def _allow_super_init(__init__): """ @@ -419,7 +422,7 @@ def _get_key(self, event): def flush_events(self): # docstring inherited - qApp.processEvents() + QtWidgets.QApplication.instance().processEvents() def start_event_loop(self, timeout=0): # docstring inherited @@ -1022,7 +1025,7 @@ def trigger(self, *args): class ToolCopyToClipboardQT(backend_tools.ToolCopyToClipboardBase): def trigger(self, *args, **kwargs): pixmap = self.canvas.grab() - qApp.clipboard().setPixmap(pixmap) + QtWidgets.QApplication.instance().clipboard().setPixmap(pixmap) FigureManagerQT._toolbar2_class = NavigationToolbar2QT @@ -1036,5 +1039,6 @@ class _BackendQT(_Backend): @staticmethod def mainloop(): - with _maybe_allow_interrupt(qApp): - qt_compat._exec(qApp) + qapp = QtWidgets.QApplication.instance() + with _maybe_allow_interrupt(qapp): + qt_compat._exec(qapp) From 88377a3b840ddcf2cdd7f5d078b7bdc7f00a634a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 22 Mar 2022 01:02:04 -0400 Subject: [PATCH 2/2] TST: add test of qApp access warning --- .../tests/test_backends_interactive.py | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 23991410601c..81b9377c750e 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -267,7 +267,9 @@ def _implqt5agg(): assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules import matplotlib.backends.backend_qt5 - matplotlib.backends.backend_qt5.qApp + with pytest.warns(DeprecationWarning, + match="QtWidgets.QApplication.instance"): + matplotlib.backends.backend_qt5.qApp def _implcairo(): @@ -279,7 +281,9 @@ def _implcairo(): assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules import matplotlib.backends.backend_qt5 - matplotlib.backends.backend_qt5.qApp + with pytest.warns(DeprecationWarning, + match="QtWidgets.QApplication.instance"): + matplotlib.backends.backend_qt5.qApp def _implcore(): @@ -289,7 +293,10 @@ def _implcore(): assert 'PyQt6' not in sys.modules assert 'pyside6' not in sys.modules assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules - matplotlib.backends.backend_qt5.qApp + + with pytest.warns(DeprecationWarning, + match="QtWidgets.QApplication.instance"): + matplotlib.backends.backend_qt5.qApp def test_qt5backends_uses_qt5(): @@ -410,11 +417,25 @@ def _lazy_headless(): @pytest.mark.skipif(sys.platform != "linux", reason="this a linux-only test") -@pytest.mark.backend('QtAgg', skip_on_importerror=True) +@pytest.mark.backend('Qt5Agg', skip_on_importerror=True) def test_lazy_linux_headless(): proc = _run_helper(_lazy_headless, timeout=_test_timeout, MPLBACKEND="") +def _qApp_warn_impl(): + import matplotlib.backends.backend_qt + import pytest + + with pytest.warns( + DeprecationWarning, match="QtWidgets.QApplication.instance"): + matplotlib.backends.backend_qt.qApp + + +@pytest.mark.backend('QtAgg', skip_on_importerror=True) +def test_qApp_warn(): + _run_helper(_qApp_warn_impl, timeout=_test_timeout) + + def _test_number_of_draws_script(): import matplotlib.pyplot as plt