diff --git a/doc/api/backend_qt_api.rst b/doc/api/backend_qt_api.rst index 474e04ef4b4b..622889c10e5c 100644 --- a/doc/api/backend_qt_api.rst +++ b/doc/api/backend_qt_api.rst @@ -1,15 +1,70 @@ :mod:`.backend_qtagg`, :mod:`.backend_qtcairo` ============================================== -**NOTE** These backends are not documented here, to avoid adding a dependency -to building the docs. +**NOTE** These backends are not (auto) documented here, to avoid adding a +dependency to building the docs. .. redirect-from:: /api/backend_qt4agg_api .. redirect-from:: /api/backend_qt4cairo_api .. redirect-from:: /api/backend_qt5agg_api .. redirect-from:: /api/backend_qt5cairo_api +.. module:: matplotlib.backends.qt_compat +.. module:: matplotlib.backends.backend_qt .. module:: matplotlib.backends.backend_qtagg .. module:: matplotlib.backends.backend_qtcairo .. module:: matplotlib.backends.backend_qt5agg .. module:: matplotlib.backends.backend_qt5cairo + +.. _QT_bindings: + +Qt Bindings +----------- + +There are currently 2 actively supported Qt versions, Qt5 and Qt6, and two +supported Python bindings per version -- `PyQt5 +`_ and `PySide2 +`_ for Qt5 and `PyQt6 +`_ and `PySide6 +`_ for Qt6 [#]_. While both PyQt +and Qt for Python (aka PySide) closely mirror the underlying C++ API they are +wrapping, they are not drop-in replacements for each other [#]_. To account +for this, Matplotlib has an internal API compatibility layer in +`matplotlib.backends.qt_compat` which covers our needs. Despite being a public +module, we do not consider this to be a stable user-facing API and it may +change without warning [#]_. + +Previously Matplotlib's Qt backends had the Qt version number in the name, both +in the module and the :rc:`backend` value +(e.g. ``matplotlib.backends.backend_qt4agg`` and +``matplotlib.backends.backend_qt5agg``). However as part of adding support for +Qt6 we were able to support both Qt5 and Qt6 with a single implementation with +all of the Qt version and binding support handled in +`~matplotlib.backends.qt_compat`. A majority of the renderer agnostic Qt code +is now in `matplotlib.backends.backend_qt` with specialization for AGG in +``backend_qtagg`` and cairo in ``backend_qtcairo``. + +The binding is selected at run time based on what bindings are already imported +(by checking for the ``QtCore`` sub-package), then by the :envvar:`QT_API` +environment variable, and finally by the :rc:`backend`. In all cases when we +need to search, the order is ``PyQt6``, ``PySide6``, ``PyQt5``, ``PySide2``. +See :ref:`QT_API-usage` for usage instructions. + +The ``backend_qt5``, ``backend_qt5agg``, and ``backend_qt5cairo`` are provided +and force the use of a Qt5 binding for backwards compatibility. Their use is +discouraged (but not deprecated) and ``backend_qt``, ``backend_qtagg``, or +``backend_qtcairo`` should be preferred instead. However, these modules will +not be deprecated until we drop support for Qt5. + + + + +.. [#] There is also `PyQt4 + `_ and `PySide + `_ for Qt4 but these are no + longer supported by Matplotlib and upstream support for Qt4 ended + in 2015. +.. [#] Despite the slight API differences, the more important distinction + between the PyQt and Qt for Python series of bindings is licensing. +.. [#] If you are looking for a general purpose compatibility library please + see `qtpy `_. diff --git a/doc/users/explain/backends.rst b/doc/users/explain/backends.rst index e42a489c707b..ca670b82f9ba 100644 --- a/doc/users/explain/backends.rst +++ b/doc/users/explain/backends.rst @@ -244,7 +244,8 @@ The :envvar:`QT_API` environment variable can be set to override the search when nothing has already been loaded. It may be set to (case-insensitively) PyQt6, PySide6, PyQt5, or PySide2 to pick the version and binding to use. If the chosen implementation is unavailable, the Qt backend will fail to load -without attempting any other Qt implementations. +without attempting any other Qt implementations. See :ref:`QT_bindings` for +more details. Using non-builtin backends -------------------------- diff --git a/lib/matplotlib/backends/__init__.py b/lib/matplotlib/backends/__init__.py index 6f4015d6ea8e..3e687f85b0be 100644 --- a/lib/matplotlib/backends/__init__.py +++ b/lib/matplotlib/backends/__init__.py @@ -1,2 +1,3 @@ # NOTE: plt.switch_backend() (called at import time) will add a "backend" # attribute here for backcompat. +_QT_FORCE_QT5_BINDING = False diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 5234b424d974..76b4d74ef640 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -114,6 +114,28 @@ def _create_qApp(): 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) diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 0774356ff8c5..3c6b2c66a845 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -1,4 +1,9 @@ -from .backend_qt import ( +from .. import backends + +backends._QT_FORCE_QT5_BINDING = True + + +from .backend_qt import ( # noqa backend_version, SPECIAL_KEYS, # Public API cursord, _create_qApp, _BackendQT, TimerQT, MainWindow, FigureCanvasQT, @@ -9,8 +14,15 @@ FigureCanvasBase, FigureManagerBase, MouseButton, NavigationToolbar2, TimerBase, ToolContainerBase, figureoptions, Gcf ) +from . import backend_qt as _backend_qt # noqa @_BackendQT.export class _BackendQT5(_BackendQT): pass + + +def __getattr__(name): + if name == 'qApp': + return _backend_qt.qApp + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index d4f618df8ea7..c81fa6f6ccb3 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -1,10 +1,11 @@ """ Render to qt from agg """ +from .. import backends -from .backend_qtagg import _BackendQTAgg -from .backend_qtagg import ( # noqa: F401 # pylint: disable=W0611 - FigureCanvasQTAgg, FigureManagerQT, NavigationToolbar2QT, +backends._QT_FORCE_QT5_BINDING = True +from .backend_qtagg import ( # noqa: F401, E402 # pylint: disable=W0611 + _BackendQTAgg, FigureCanvasQTAgg, FigureManagerQT, NavigationToolbar2QT, backend_version, FigureCanvasAgg, FigureCanvasQT ) diff --git a/lib/matplotlib/backends/backend_qt5cairo.py b/lib/matplotlib/backends/backend_qt5cairo.py index 02cf9920ce61..a4263f597119 100644 --- a/lib/matplotlib/backends/backend_qt5cairo.py +++ b/lib/matplotlib/backends/backend_qt5cairo.py @@ -1,6 +1,9 @@ -from .backend_qtcairo import _BackendQTCairo -from .backend_qtcairo import ( # noqa: F401 # pylint: disable=W0611 - FigureCanvasQTCairo, FigureCanvasCairo, FigureCanvasQT) +from .. import backends + +backends._QT_FORCE_QT5_BINDING = True +from .backend_qtcairo import ( # noqa: F401, E402 # pylint: disable=W0611 + _BackendQTCairo, FigureCanvasQTCairo, FigureCanvasCairo, FigureCanvasQT +) @_BackendQTCairo.export diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index fd35b31dd7e1..47c1cedff741 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -25,6 +25,7 @@ import matplotlib as mpl from matplotlib import _api +from . import _QT_FORCE_QT5_BINDING QT_API_PYQT6 = "PyQt6" QT_API_PYSIDE6 = "PySide6" @@ -57,10 +58,16 @@ # requested backend actually matches). Use dict.__getitem__ to avoid # triggering backend resolution (which can result in a partially but # incompletely imported backend_qt5). -elif dict.__getitem__(mpl.rcParams, "backend") in ["Qt5Agg", "Qt5Cairo"]: +elif ( + isinstance(dict.__getitem__(mpl.rcParams, "backend"), str) and + dict.__getitem__(mpl.rcParams, "backend").lower() in [ + "qt5agg", "qt5cairo" + ] +): 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). @@ -112,12 +119,19 @@ def _isdeleted(obj): 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__. - _candidates = [ - (_setup_pyqt5plus, QT_API_PYQT6), - (_setup_pyqt5plus, QT_API_PYSIDE6), - (_setup_pyqt5plus, QT_API_PYQT5), - (_setup_pyqt5plus, QT_API_PYSIDE2), - ] + 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() diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 60887b574645..48131a5b1950 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -104,7 +104,6 @@ def _copy_docstring_and_deprecators(method, func=None): ## Global ## - _IP_REGISTERED = None _INSTALL_FIG_OBSERVER = False @@ -207,6 +206,28 @@ def _get_required_interactive_framework(backend_mod): # Inline this once the deprecation elapses. return backend_mod.FigureCanvas.required_interactive_framework +_backend_mod = None + + +def _get_backend_mod(): + """ + Ensure that a backend is selected and return it. + + This is currently private, but may be made public in the future. + """ + if _backend_mod is None: + # Use __getitem__ here to avoid going through the fallback logic (which + # will (re)import pyplot and then call switch_backend if we need to + # resolve the auto sentinel) + switch_backend(dict.__getitem__(rcParams, "backend")) + # Just to be safe. Interactive mode can be turned on without calling + # `plt.ion()` so register it again here. This is safe because multiple + # calls to `install_repl_displayhook` are no-ops and the registered + # function respects `mpl.is_interactive()` to determine if it should + # trigger a draw. + install_repl_displayhook() + return _backend_mod + def switch_backend(newbackend): """ @@ -297,7 +318,7 @@ class backend_mod(matplotlib.backend_bases._Backend): def _warn_if_gui_out_of_main_thread(): - if (_get_required_interactive_framework(_backend_mod) + if (_get_required_interactive_framework(_get_backend_mod()) and threading.current_thread() is not threading.main_thread()): _api.warn_external( "Starting a Matplotlib GUI outside of the main thread will likely " @@ -308,7 +329,7 @@ def _warn_if_gui_out_of_main_thread(): def new_figure_manager(*args, **kwargs): """Create a new figure manager instance.""" _warn_if_gui_out_of_main_thread() - return _backend_mod.new_figure_manager(*args, **kwargs) + return _get_backend_mod().new_figure_manager(*args, **kwargs) # This function's signature is rewritten upon backend-load by switch_backend. @@ -321,7 +342,7 @@ def draw_if_interactive(*args, **kwargs): End users will typically not have to call this function because the the interactive mode takes care of this. """ - return _backend_mod.draw_if_interactive(*args, **kwargs) + return _get_backend_mod().draw_if_interactive(*args, **kwargs) # This function's signature is rewritten upon backend-load by switch_backend. @@ -370,7 +391,7 @@ def show(*args, **kwargs): explicitly there. """ _warn_if_gui_out_of_main_thread() - return _backend_mod.show(*args, **kwargs) + return _get_backend_mod().show(*args, **kwargs) def isinteractive(): @@ -2226,15 +2247,6 @@ def polar(*args, **kwargs): set(_interactive_bk) - {'WebAgg', 'nbAgg'}) and cbook._get_running_interactive_framework()): dict.__setitem__(rcParams, "backend", rcsetup._auto_backend_sentinel) -# Set up the backend. -switch_backend(rcParams["backend"]) - -# Just to be safe. Interactive mode can be turned on without -# calling `plt.ion()` so register it again here. -# This is safe because multiple calls to `install_repl_displayhook` -# are no-ops and the registered function respect `mpl.is_interactive()` -# to determine if they should trigger a draw. -install_repl_displayhook() ################# REMAINING CONTENT GENERATED BY boilerplate.py ############## diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index 754277c41f43..eba878e0a4a3 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -1,12 +1,13 @@ """ Helper functions for testing. """ - +from pathlib import Path +from tempfile import TemporaryDirectory import locale import logging +import os import subprocess -from pathlib import Path -from tempfile import TemporaryDirectory +import sys import matplotlib as mpl from matplotlib import _api @@ -49,6 +50,46 @@ def setup(): set_reproducibility_for_testing() +def subprocess_run_helper(func, *args, timeout, **extra_env): + """ + Run a function in a sub-process + + Parameters + ---------- + func : function + The function to be run. It must be in a module that is importable. + + *args : str + Any additional command line arguments to be passed in + the first argument to subprocess.run + + **extra_env : Dict[str, str] + Any additional envromental variables to be set for + the subprocess. + + """ + target = func.__name__ + module = func.__module__ + proc = subprocess.run( + [sys.executable, + "-c", + f""" +from {module} import {target} +{target}() +""", + *args], + env={ + **os.environ, + "SOURCE_DATE_EPOCH": "0", + **extra_env + }, + timeout=timeout, check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True) + return proc + + def _check_for_pgf(texsystem): """ Check if a given TeX system + pgf is available diff --git a/lib/matplotlib/tests/test_backend_tk.py b/lib/matplotlib/tests/test_backend_tk.py index c3dac0556087..f7bb141d2541 100644 --- a/lib/matplotlib/tests/test_backend_tk.py +++ b/lib/matplotlib/tests/test_backend_tk.py @@ -64,6 +64,7 @@ def test_func(): def test_blit(): # pragma: no cover import matplotlib.pyplot as plt import numpy as np + import matplotlib.backends.backend_tkagg # noqa from matplotlib.backends import _tkagg fig, ax = plt.subplots() diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 346e0e2b967e..2818f3d21cca 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -14,6 +14,7 @@ import matplotlib as mpl from matplotlib import _c_internal_utils +from matplotlib.testing import subprocess_run_helper as _run_helper # Minimal smoke-testing of the backends for which the dependencies are @@ -87,8 +88,8 @@ def _test_interactive_impl(): "webagg.open_in_browser": False, "webagg.port_retries": 1, }) - if len(sys.argv) >= 2: # Second argument is json-encoded rcParams. - rcParams.update(json.loads(sys.argv[1])) + + rcParams.update(json.loads(sys.argv[1])) backend = plt.rcParams["backend"].lower() assert_equal = TestCase().assertEqual assert_raises = TestCase().assertRaises @@ -163,27 +164,16 @@ def test_interactive_backend(env, toolbar): if env["MPLBACKEND"] == "macosx": if toolbar == "toolmanager": pytest.skip("toolmanager is not implemented for macosx.") + proc = _run_helper(_test_interactive_impl, + json.dumps({"toolbar": toolbar}), + timeout=_test_timeout, + **env) - proc = subprocess.run( - [sys.executable, "-c", - inspect.getsource(_test_interactive_impl) - + "\n_test_interactive_impl()", - json.dumps({"toolbar": toolbar})], - env={**os.environ, "SOURCE_DATE_EPOCH": "0", **env}, - timeout=_test_timeout, - stdout=subprocess.PIPE, universal_newlines=True) - if proc.returncode: - pytest.fail("The subprocess returned with non-zero exit status " - f"{proc.returncode}.") assert proc.stdout.count("CloseEvent") == 1 -# The source of this function gets extracted and run in another process, so it -# must be fully self-contained. def _test_thread_impl(): from concurrent.futures import ThreadPoolExecutor - import json - import sys from matplotlib import pyplot as plt, rcParams @@ -191,8 +181,6 @@ def _test_thread_impl(): "webagg.open_in_browser": False, "webagg.port_retries": 1, }) - if len(sys.argv) >= 2: # Second argument is json-encoded rcParams. - rcParams.update(json.loads(sys.argv[1])) # Test artist creation and drawing does not crash from thread # No other guarantees! @@ -246,15 +234,125 @@ def _test_thread_impl(): @pytest.mark.parametrize("env", _thread_safe_backends) @pytest.mark.flaky(reruns=3) def test_interactive_thread_safety(env): - proc = subprocess.run( - [sys.executable, "-c", - inspect.getsource(_test_thread_impl) + "\n_test_thread_impl()"], - env={**os.environ, "SOURCE_DATE_EPOCH": "0", **env}, - timeout=_test_timeout, check=True, - stdout=subprocess.PIPE, universal_newlines=True) + proc = _run_helper(_test_thread_impl, + timeout=_test_timeout, **env) assert proc.stdout.count("CloseEvent") == 1 +def _impl_test_lazy_auto_backend_selection(): + import matplotlib + import matplotlib.pyplot as plt + # just importing pyplot should not be enough to trigger resolution + bk = dict.__getitem__(matplotlib.rcParams, 'backend') + assert not isinstance(bk, str) + assert plt._backend_mod is None + # but actually plotting should + plt.plot(5) + assert plt._backend_mod is not None + bk = dict.__getitem__(matplotlib.rcParams, 'backend') + assert isinstance(bk, str) + + +def test_lazy_auto_backend_selection(): + _run_helper(_impl_test_lazy_auto_backend_selection, + timeout=_test_timeout) + + +def _implqt5agg(): + import matplotlib.backends.backend_qt5agg # noqa + import sys + + assert 'PyQt6' not in sys.modules + assert 'pyside6' not in sys.modules + assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules + + import matplotlib.backends.backend_qt5 + matplotlib.backends.backend_qt5.qApp + + +def _implcairo(): + import matplotlib.backends.backend_qt5cairo # noqa + import sys + + assert 'PyQt6' not in sys.modules + assert 'pyside6' not in sys.modules + assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules + + import matplotlib.backends.backend_qt5 + matplotlib.backends.backend_qt5.qApp + + +def _implcore(): + import matplotlib.backends.backend_qt5 + import sys + + 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 + + +def test_qt5backends_uses_qt5(): + qt5_bindings = [ + dep for dep in ['PyQt5', 'pyside2'] + if importlib.util.find_spec(dep) is not None + ] + qt6_bindings = [ + dep for dep in ['PyQt6', 'pyside6'] + if importlib.util.find_spec(dep) is not None + ] + if len(qt5_bindings) == 0 or len(qt6_bindings) == 0: + pytest.skip('need both QT6 and QT5 bindings') + _run_helper(_implqt5agg, timeout=_test_timeout) + if importlib.util.find_spec('pycairo') is not None: + _run_helper(_implcairo, timeout=_test_timeout) + _run_helper(_implcore, timeout=_test_timeout) + + +def _impl_test_cross_Qt_imports(): + import sys + import importlib + import pytest + + _, host_binding, mpl_binding = sys.argv + # import the mpl binding. This will force us to use that binding + importlib.import_module(f'{mpl_binding}.QtCore') + mpl_binding_qwidgets = importlib.import_module(f'{mpl_binding}.QtWidgets') + import matplotlib.backends.backend_qt + host_qwidgets = importlib.import_module(f'{host_binding}.QtWidgets') + + host_app = host_qwidgets.QApplication(["mpl testing"]) + with pytest.warns(UserWarning, match="Mixing Qt major"): + matplotlib.backends.backend_qt._create_qApp() + + +def test_cross_Qt_imports(): + qt5_bindings = [ + dep for dep in ['PyQt5', 'PySide2'] + if importlib.util.find_spec(dep) is not None + ] + qt6_bindings = [ + dep for dep in ['PyQt6', 'PySide6'] + if importlib.util.find_spec(dep) is not None + ] + if len(qt5_bindings) == 0 or len(qt6_bindings) == 0: + pytest.skip('need both QT6 and QT5 bindings') + + for qt5 in qt5_bindings: + for qt6 in qt6_bindings: + for pair in ([qt5, qt6], [qt6, qt5]): + try: + _run_helper(_impl_test_cross_Qt_imports, + *pair, + timeout=_test_timeout) + except subprocess.CalledProcessError as ex: + # if segfault, carry on. We do try to warn the user they + # are doing something that we do not expect to work + if ex.returncode == -11: + continue + raise + + @pytest.mark.skipif('TF_BUILD' in os.environ, reason="this test fails an azure for unknown reasons") @pytest.mark.skipif(os.name == "nt", reason="Cannot send SIGINT on Windows.") @@ -263,7 +361,7 @@ def test_webagg(): proc = subprocess.Popen( [sys.executable, "-c", inspect.getsource(_test_interactive_impl) - + "\n_test_interactive_impl()"], + + "\n_test_interactive_impl()", "{}"], env={**os.environ, "MPLBACKEND": "webagg", "SOURCE_DATE_EPOCH": "0"}) url = "http://{}:{}".format( mpl.rcParams["webagg.address"], mpl.rcParams["webagg.port"]) @@ -285,37 +383,33 @@ def test_webagg(): assert proc.wait(timeout=_test_timeout) == 0 +def _lazy_headless(): + import os + import sys + + # make it look headless + os.environ.pop('DISPLAY', None) + os.environ.pop('WAYLAND_DISPLAY', None) + + # we should fast-track to Agg + import matplotlib.pyplot as plt + plt.get_backend() == 'agg' + assert 'PyQt5' not in sys.modules + + # make sure we really have pyqt installed + import PyQt5 # noqa + assert 'PyQt5' in sys.modules + + # try to switch and make sure we fail with ImportError + try: + plt.switch_backend('qt5agg') + except ImportError: + ... + else: + sys.exit(1) + + @pytest.mark.skipif(sys.platform != "linux", reason="this a linux-only test") @pytest.mark.backend('QtAgg', skip_on_importerror=True) def test_lazy_linux_headless(): - test_script = """ -import os -import sys - -# make it look headless -os.environ.pop('DISPLAY', None) -os.environ.pop('WAYLAND_DISPLAY', None) - -# we should fast-track to Agg -import matplotlib.pyplot as plt -plt.get_backend() == 'agg' -assert 'PyQt5' not in sys.modules - -# make sure we really have pyqt installed -import PyQt5 -assert 'PyQt5' in sys.modules - -# try to switch and make sure we fail with ImportError -try: - plt.switch_backend('qt5agg') -except ImportError: - ... -else: - sys.exit(1) - -""" - proc = subprocess.run([sys.executable, "-c", test_script], - env={**os.environ, "MPLBACKEND": ""}) - if proc.returncode: - pytest.fail("The subprocess returned with non-zero exit status " - f"{proc.returncode}.") + proc = _run_helper(_lazy_headless, timeout=_test_timeout, MPLBACKEND="") diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 2b2e36e2a516..75b6f727f799 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -497,7 +497,8 @@ def test_backend_fallback_headless(tmpdir): [sys.executable, "-c", "import matplotlib;" "matplotlib.use('tkagg');" - "import matplotlib.pyplot" + "import matplotlib.pyplot;" + "matplotlib.pyplot.plot(42);" ], env=env, check=True, stderr=subprocess.DEVNULL) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy