From 56acb0f691f3447f7f15b257b164e0bc431760eb Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 4 Jul 2018 23:04:19 +0200 Subject: [PATCH 1/7] ENH: backend switching. See changes documented in the API changes file. I inlined pylab_setup into switch_backend (and deprecated the old version of pylab_setup) because otherwise the typical call stack would be `use()` -> `switch_backend()` -> `pylab_setup()`, which is a bit of a mess; at least we can get rid of one of the layers. --- .../2018-02-15-AL-deprecations.rst | 1 + lib/matplotlib/__init__.py | 99 +++++++++---------- lib/matplotlib/backend_bases.py | 7 +- lib/matplotlib/backends/__init__.py | 3 + lib/matplotlib/pyplot.py | 75 +++++++++++--- lib/matplotlib/rcsetup.py | 15 +-- .../tests/test_backends_interactive.py | 49 ++++++++- matplotlibrc.template | 5 +- setup.py | 4 +- 9 files changed, 169 insertions(+), 89 deletions(-) diff --git a/doc/api/next_api_changes/2018-02-15-AL-deprecations.rst b/doc/api/next_api_changes/2018-02-15-AL-deprecations.rst index 654a017e2303..e2c9e3fb693c 100644 --- a/doc/api/next_api_changes/2018-02-15-AL-deprecations.rst +++ b/doc/api/next_api_changes/2018-02-15-AL-deprecations.rst @@ -22,6 +22,7 @@ The following classes, methods, functions, and attributes are deprecated: handle autorepeated key presses). - ``backend_qt5.error_msg_qt``, ``backend_qt5.exception_handler``, - ``backend_wx.FigureCanvasWx.macros``, +- ``backends.pylab_setup``, - ``cbook.GetRealpathAndStat``, ``cbook.Locked``, - ``cbook.is_numlike`` (use ``isinstance(..., numbers.Number)`` instead), ``cbook.listFiles``, ``cbook.unicode_safe``, diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 3eee3e0116f6..3f2a6a300e30 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1301,74 +1301,63 @@ def __exit__(self, exc_type, exc_value, exc_tb): dict.update(rcParams, self._orig) -_use_error_msg = """ -This call to matplotlib.use() has no effect because the backend has already -been chosen; matplotlib.use() must be called *before* pylab, matplotlib.pyplot, -or matplotlib.backends is imported for the first time. - -The backend was *originally* set to {backend!r} by the following code: -{tb} -""" - - def use(arg, warn=True, force=False): """ Set the matplotlib backend to one of the known backends. - The argument is case-insensitive. *warn* specifies whether a - warning should be issued if a backend has already been set up. - *force* is an **experimental** flag that tells matplotlib to - attempt to initialize a new backend by reloading the backend - module. + To find out which backend is currently set, see + :func:`matplotlib.get_backend`. + + + Parameters + ---------- + arg : str + The backend to switch to. This can either be one of the + 'standard' backend names or a string of the form + ``module://my.module.name``. This value is case-insensitive. - .. note:: + warn : bool, optional + If True, warn if this is called after pyplot has been imported + and a backend is set up. - This function must be called *before* importing pyplot for - the first time; or, if you are not using pyplot, it must be called - before importing matplotlib.backends. If warn is True, a warning - is issued if you try and call this after pylab or pyplot have been - loaded. In certain black magic use cases, e.g. - :func:`pyplot.switch_backend`, we are doing the reloading necessary to - make the backend switch work (in some cases, e.g., pure image - backends) so one can set warn=False to suppress the warnings. + defaults to True + + force : bool, optional + If True, attempt to switch the backend. This defaults to + false and using `.pyplot.switch_backend` is preferred. - To find out which backend is currently set, see - :func:`matplotlib.get_backend`. """ - # Lets determine the proper backend name first - if arg.startswith('module://'): - name = arg - else: - # Lowercase only non-module backend names (modules are case-sensitive) - arg = arg.lower() - name = validate_backend(arg) - - # Check if we've already set up a backend - if 'matplotlib.backends' in sys.modules: - # Warn only if called with a different name - if (rcParams['backend'] != name) and warn: - import matplotlib.backends + name = validate_backend(arg) + + # if setting back to the same thing, do nothing + if (rcParams['backend'] == name): + pass + + # Check if we have already imported pyplot and triggered + # backend selection, do a bit more work + elif 'matplotlib.pyplot' in sys.modules: + # If we are here then the requested is different than the current. + # If we are going to force the switch, never warn, else, if warn + # is True, then direct users to `plt.switch_backend` + if (not force) and warn: warnings.warn( - _use_error_msg.format( - backend=rcParams['backend'], - tb=matplotlib.backends._backend_loading_tb), + ("matplotlib.pyplot as already been imported, " + "this call will have no effect."), stacklevel=2) - # Unless we've been told to force it, just return - if not force: - return - need_reload = True + # if we are going to force switching the backend, pull in + # `switch_backend` from pyplot. This will only happen if + # pyplot is already imported. + if force: + from matplotlib.pyplot import switch_backend + switch_backend(name) + # Finally if pyplot is not imported update both rcParams and + # rcDefaults so restoring the defaults later with rcdefaults + # won't change the backend. This is a bit of overkill as 'backend' + # is already in style.core.STYLE_BLACKLIST, but better to be safe. else: - need_reload = False - - # Store the backend name - rcParams['backend'] = name - - # If needed we reload here because a lot of setup code is triggered on - # module import. See backends/__init__.py for more detail. - if need_reload: - importlib.reload(sys.modules['matplotlib.backends']) + rcParams['backend'] = rcParamsDefault['backend'] = name if os.environ.get('MPLBACKEND'): diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index ce15d9295fac..9810178ee38b 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3183,6 +3183,10 @@ class _Backend(object): # class FooBackend(_Backend): # # override the attributes and methods documented below. + # Set to one of {"qt5", "qt4", "gtk3", "wx", "tk", "macosx"} if an + # interactive framework is required, or None otherwise. + required_interactive_framework = None + # `backend_version` may be overridden by the subclass. backend_version = "unknown" @@ -3265,7 +3269,8 @@ def show(cls, block=None): @staticmethod def export(cls): - for name in ["backend_version", + for name in ["required_interactive_framework", + "backend_version", "FigureCanvas", "FigureManager", "new_figure_manager", diff --git a/lib/matplotlib/backends/__init__.py b/lib/matplotlib/backends/__init__.py index 2467a4235373..9be23abe518b 100644 --- a/lib/matplotlib/backends/__init__.py +++ b/lib/matplotlib/backends/__init__.py @@ -5,11 +5,13 @@ import traceback import matplotlib +from matplotlib import cbook from matplotlib.backend_bases import _Backend _log = logging.getLogger(__name__) backend = matplotlib.get_backend() +# FIXME: Remove. _backend_loading_tb = "".join( line for line in traceback.format_stack() # Filter out line noise from importlib line. @@ -64,6 +66,7 @@ def _get_running_interactive_framework(): return None +@cbook.deprecated("3.0") def pylab_setup(name=None): """ Return new_figure_manager, draw_if_interactive and show for pyplot. diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 3096473ca63a..7fd08c29b27d 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -18,7 +18,9 @@ The object-oriented API is recommended for more complex plots. """ +import importlib import inspect +import logging from numbers import Number import re import sys @@ -29,7 +31,7 @@ import matplotlib import matplotlib.colorbar import matplotlib.image -from matplotlib import style +from matplotlib import rcsetup, style from matplotlib import _pylab_helpers, interactive from matplotlib.cbook import ( dedent, deprecated, silent_list, warn_deprecated, _string_to_bool) @@ -67,10 +69,13 @@ MaxNLocator from matplotlib.backends import pylab_setup +_log = logging.getLogger(__name__) + ## Backend detection ## +# FIXME: Deprecate. def _backend_selection(): """ If rcParams['backend_fallback'] is true, check to see if the @@ -110,8 +115,6 @@ def _backend_selection(): ## Global ## -_backend_mod, new_figure_manager, draw_if_interactive, _show = pylab_setup() - _IP_REGISTERED = None _INSTALL_FIG_OBSERVER = False @@ -213,21 +216,60 @@ def findobj(o=None, match=None, include_self=True): def switch_backend(newbackend): """ - Switch the default backend. This feature is **experimental**, and - is only expected to work switching to an image backend. e.g., if - you have a bunch of PostScript scripts that you want to run from - an interactive ipython session, you may want to switch to the PS - backend before running them to avoid having a bunch of GUI windows - popup. If you try to interactively switch from one GUI backend to - another, you will explode. + Close all open figures and set the Matplotlib backend. - Calling this command will close all open windows. + The argument is case-insensitive. Switching to an interactive backend is + possible only if no event loop for another interactive backend has started. + Switching to and from non-interactive backends is always possible. + + Parameters + ---------- + newbackend : str + The name of the backend to use. """ - close('all') + close("all") + + if newbackend is rcsetup._auto_backend_sentinel: + for candidate in ["macosx", "qt5agg", "qt4agg", "gtk3agg", "gtk3cairo", + "tkagg", "wxagg", "agg", "cairo"]: + try: + switch_backend(candidate) + except ImportError: + continue + else: + return + + backend_name = ( + newbackend[9:] if newbackend.startswith("module://") + else "matplotlib.backends.backend_{}".format(newbackend.lower())) + + backend_mod = importlib.import_module(backend_name) + Backend = type( + "Backend", (matplotlib.backends._Backend,), vars(backend_mod)) + _log.info("Loaded backend %s version %s.", + newbackend, Backend.backend_version) + + required_framework = Backend.required_interactive_framework + current_framework = \ + matplotlib.backends._get_running_interactive_framework() + if (current_framework and required_framework + and current_framework != required_framework): + raise ImportError( + "Cannot load backend {!r} which requires the {!r} interactive " + "framework, as {!r} is currently running".format( + newbackend, required_framework, current_framework)) + + rcParams['backend'] = rcParamsDefault['backend'] = newbackend + global _backend_mod, new_figure_manager, draw_if_interactive, _show - matplotlib.use(newbackend, warn=False, force=True) - from matplotlib.backends import pylab_setup - _backend_mod, new_figure_manager, draw_if_interactive, _show = pylab_setup() + _backend_mod = backend_mod + new_figure_manager = Backend.new_figure_manager + draw_if_interactive = Backend.draw_if_interactive + _show = Backend.show + + # Need to keep a global reference to the backend for compatibility reasons. + # See https://github.com/matplotlib/matplotlib/issues/6092 + matplotlib.backends.backend = newbackend def show(*args, **kw): @@ -2364,6 +2406,9 @@ def _autogen_docstring(base): # to determine if they should trigger a draw. install_repl_displayhook() +# Set up the backend. +switch_backend(rcParams["backend"]) + ################# REMAINING CONTENT GENERATED BY boilerplate.py ############## diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index e3c58f48026a..3b532b91d732 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -18,6 +18,7 @@ import operator import os import re +import sys from matplotlib import cbook from matplotlib.cbook import ls_mapper @@ -242,13 +243,14 @@ def validate_fonttype(s): _validate_standard_backends = ValidateInStrings( 'backend', all_backends, ignorecase=True) +_auto_backend_sentinel = object() def validate_backend(s): - if s.startswith('module://'): - return s - else: - return _validate_standard_backends(s) + backend = ( + s if s is _auto_backend_sentinel or s.startswith("module://") + else _validate_standard_backends(s)) + return backend def validate_qt4(s): @@ -965,9 +967,8 @@ def _validate_linestyle(ls): # a map from key -> value, converter defaultParams = { - 'backend': ['Agg', validate_backend], # agg is certainly - # present - 'backend_fallback': [True, validate_bool], # agg is certainly present + 'backend': [_auto_backend_sentinel, validate_backend], + 'backend_fallback': [True, validate_bool], 'backend.qt4': [None, validate_qt4], 'backend.qt5': [None, validate_qt5], 'webagg.port': [8988, validate_int], diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index ccc2c31b3133..5652e18a525c 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -21,7 +21,6 @@ def _get_testable_interactive_backends(): for deps, backend in [ # (["cairocffi", "pgi"], "gtk3agg"), (["cairocffi", "pgi"], "gtk3cairo"), (["PyQt5"], "qt5agg"), - (["cairocffi", "PyQt5"], "qt5cairo"), (["tkinter"], "tkagg"), (["wx"], "wx"), (["wx"], "wxagg")]: @@ -43,15 +42,55 @@ def _get_testable_interactive_backends(): # early. Also, gtk3 redefines key_press_event with a different signature, so # we directly invoke it from the superclass instead. _test_script = """\ +import importlib import sys +from unittest import TestCase + +import matplotlib as mpl from matplotlib import pyplot as plt, rcParams from matplotlib.backend_bases import FigureCanvasBase rcParams.update({ "webagg.open_in_browser": False, "webagg.port_retries": 1, }) +backend = plt.rcParams["backend"].lower() +assert_equal = TestCase().assertEqual +assert_raises = TestCase().assertRaises + +if backend.endswith("agg") and not backend.startswith(("gtk3", "web")): + # Force interactive framework setup. + plt.figure() + + # Check that we cannot switch to a backend using another interactive + # framework, but can switch to a backend using cairo instead of agg, or a + # non-interactive backend. In the first case, we use tkagg as the "other" + # interactive backend as it is (essentially) guaranteed to be present. + # Moreover, don't test switching away from gtk3 as Gtk.main_level() is + # not set up at this point yet, and webagg, which uses no interactive + # framework. + + if backend != "tkagg": + with assert_raises(ImportError): + mpl.use("tkagg") + + def check_alt_backend(alt_backend): + mpl.use(alt_backend) + fig = plt.figure() + assert_equal( + type(fig.canvas).__module__, + "matplotlib.backends.backend_{}".format(alt_backend)) + + if importlib.util.find_spec("cairocffi"): + check_alt_backend(backend[:-3] + "cairo") + check_alt_backend("svg") + +mpl.use(backend) fig, ax = plt.subplots() +assert_equal( + type(fig.canvas).__module__, + "matplotlib.backends.backend_{}".format(backend)) + ax.plot([0, 1], [2, 3]) timer = fig.canvas.new_timer(1) @@ -67,10 +106,10 @@ def _get_testable_interactive_backends(): @pytest.mark.parametrize("backend", _get_testable_interactive_backends()) @pytest.mark.flaky(reruns=3) def test_interactive_backend(backend): - subprocess.run([sys.executable, "-c", _test_script], - env={**os.environ, "MPLBACKEND": backend}, - check=True, # Throw on failure. - timeout=_test_timeout) + if subprocess.run([sys.executable, "-c", _test_script], + env={**os.environ, "MPLBACKEND": backend}, + timeout=_test_timeout).returncode: + pytest.fail("The subprocess returned an error.") @pytest.mark.skipif(os.name == "nt", reason="Cannot send SIGINT on Windows.") diff --git a/matplotlibrc.template b/matplotlibrc.template index aa864b872998..889f42937ffb 100644 --- a/matplotlibrc.template +++ b/matplotlibrc.template @@ -35,9 +35,8 @@ ## referring to the module name (which must be in the PYTHONPATH) as ## 'module://my_backend'. ## -## If you omit this parameter, it will always default to "Agg", which is a -## non-interactive backend. -backend : $TEMPLATE_BACKEND +## If you omit this parameter, the backend will be determined by fallback. +#backend : Agg ## Note that this can be overridden by the environment variable ## QT_API used by Enthought Tool Suite (ETS); valid values are diff --git a/setup.py b/setup.py index 6323a0fe111b..d13406c992e5 100644 --- a/setup.py +++ b/setup.py @@ -208,10 +208,8 @@ def run(self): default_backend = setupext.options['backend'] with open('matplotlibrc.template') as fd: template = fd.read() - template = Template(template) with open('lib/matplotlib/mpl-data/matplotlibrc', 'w') as fd: - fd.write( - template.safe_substitute(TEMPLATE_BACKEND=default_backend)) + fd.write(template) # Finalize the extension modules so they can get the Numpy include # dirs From ec77ca215b2f306a9fa38e58b57587d7803015cf Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 9 Jul 2018 10:05:11 +0200 Subject: [PATCH 2/7] Lazy-init the OSX event loop. --- lib/matplotlib/backends/__init__.py | 3 -- src/_macosx.m | 65 ++++++++++++++++++----------- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/lib/matplotlib/backends/__init__.py b/lib/matplotlib/backends/__init__.py index 9be23abe518b..e4e6082e7d79 100644 --- a/lib/matplotlib/backends/__init__.py +++ b/lib/matplotlib/backends/__init__.py @@ -56,9 +56,6 @@ def _get_running_interactive_framework(): except ImportError: pass else: - # Note that the NSApp event loop is also running when a non-native - # toolkit (e.g. Qt5) is active, but in that case we want to report the - # other toolkit; thus, this check comes after the other toolkits. if _macosx.event_loop_is_running(): return "macosx" if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"): diff --git a/src/_macosx.m b/src/_macosx.m index 8d23fdcd43ff..416cf6a583e3 100644 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -267,6 +267,39 @@ - (int)index; /* ---------------------------- Python classes ---------------------------- */ +static bool backend_inited = false; + +static void lazy_init(void) { + if (backend_inited) { + return; + } + backend_inited = true; + + NSApp = [NSApplication sharedApplication]; + + PyOS_InputHook = wait_for_stdin; + + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + WindowServerConnectionManager* connectionManager = [WindowServerConnectionManager sharedManager]; + NSWorkspace* workspace = [NSWorkspace sharedWorkspace]; + NSNotificationCenter* notificationCenter = [workspace notificationCenter]; + [notificationCenter addObserver: connectionManager + selector: @selector(launch:) + name: NSWorkspaceDidLaunchApplicationNotification + object: nil]; + [pool release]; +} + +static PyObject* +event_loop_is_running(PyObject* self) +{ + if (backend_inited) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +} + static CGFloat _get_device_scale(CGContextRef cr) { CGSize pixelSize = CGContextConvertSizeToDeviceSpace(cr, CGSizeMake(1, 1)); @@ -281,6 +314,7 @@ static CGFloat _get_device_scale(CGContextRef cr) static PyObject* FigureCanvas_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { + lazy_init(); FigureCanvas *self = (FigureCanvas*)type->tp_alloc(type, 0); if (!self) return NULL; self->view = [View alloc]; @@ -641,6 +675,7 @@ static CGFloat _get_device_scale(CGContextRef cr) static PyObject* FigureManager_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { + lazy_init(); Window* window = [Window alloc]; if (!window) return NULL; FigureManager *self = (FigureManager*)type->tp_alloc(type, 0); @@ -1076,6 +1111,7 @@ -(void)save_figure:(id)sender static PyObject* NavigationToolbar2_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { + lazy_init(); NavigationToolbar2Handler* handler = [NavigationToolbar2Handler alloc]; if (!handler) return NULL; NavigationToolbar2 *self = (NavigationToolbar2*)type->tp_alloc(type, 0); @@ -2310,16 +2346,6 @@ - (int)index } @end -static PyObject* -event_loop_is_running(PyObject* self) -{ - if ([NSApp isRunning]) { - Py_RETURN_TRUE; - } else { - Py_RETURN_FALSE; - } -} - static PyObject* show(PyObject* self) { @@ -2346,6 +2372,7 @@ - (int)index static PyObject* Timer_new(PyTypeObject* type, PyObject *args, PyObject *kwds) { + lazy_init(); Timer* self = (Timer*)type->tp_alloc(type, 0); if (!self) return NULL; self->timer = NULL; @@ -2572,7 +2599,7 @@ static bool verify_framework(void) {"event_loop_is_running", (PyCFunction)event_loop_is_running, METH_NOARGS, - "Return whether the NSApp main event loop is currently running." + "Return whether the OSX backend has set up the NSApp main event loop." }, {"show", (PyCFunction)show, @@ -2617,13 +2644,12 @@ static bool verify_framework(void) || PyType_Ready(&TimerType) < 0) return NULL; - NSApp = [NSApplication sharedApplication]; - if (!verify_framework()) return NULL; module = PyModule_Create(&moduledef); - if (module==NULL) return NULL; + if (!module) + return NULL; Py_INCREF(&FigureCanvasType); Py_INCREF(&FigureManagerType); @@ -2634,16 +2660,5 @@ static bool verify_framework(void) PyModule_AddObject(module, "NavigationToolbar2", (PyObject*) &NavigationToolbar2Type); PyModule_AddObject(module, "Timer", (PyObject*) &TimerType); - PyOS_InputHook = wait_for_stdin; - - NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; - WindowServerConnectionManager* connectionManager = [WindowServerConnectionManager sharedManager]; - NSWorkspace* workspace = [NSWorkspace sharedWorkspace]; - NSNotificationCenter* notificationCenter = [workspace notificationCenter]; - [notificationCenter addObserver: connectionManager - selector: @selector(launch:) - name: NSWorkspaceDidLaunchApplicationNotification - object: nil]; - [pool release]; return module; } From d5576e22a8e5702706313dbd707659a3de3f4b5a Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 9 Jul 2018 11:14:57 +0200 Subject: [PATCH 3/7] Don't fail Qt tests if bindings not installed. --- lib/matplotlib/tests/test_backend_qt4.py | 20 ++++++++++++++------ lib/matplotlib/tests/test_backend_qt5.py | 15 +++++++++++---- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/tests/test_backend_qt4.py b/lib/matplotlib/tests/test_backend_qt4.py index 6f7ad8ccff54..253c7768b250 100644 --- a/lib/matplotlib/tests/test_backend_qt4.py +++ b/lib/matplotlib/tests/test_backend_qt4.py @@ -1,18 +1,26 @@ import copy -from unittest.mock import Mock +from unittest import mock +import matplotlib from matplotlib import pyplot as plt from matplotlib._pylab_helpers import Gcf -import matplotlib import pytest -with matplotlib.rc_context(rc={'backend': 'Qt4Agg'}): - qt_compat = pytest.importorskip('matplotlib.backends.qt_compat') +try: + import PyQt4 +except (ImportError, RuntimeError): # RuntimeError if PyQt5 already imported. + try: + import PySide + except ImportError: + pytestmark = pytest.mark.skip("Failed to import a Qt4 binding.") + +qt_compat = pytest.importorskip('matplotlib.backends.qt_compat') +QtCore = qt_compat.QtCore + from matplotlib.backends.backend_qt4 import ( MODIFIER_KEYS, SUPER, ALT, CTRL, SHIFT) # noqa -QtCore = qt_compat.QtCore _, ControlModifier, ControlKey = MODIFIER_KEYS[CTRL] _, AltModifier, AltKey = MODIFIER_KEYS[ALT] _, SuperModifier, SuperKey = MODIFIER_KEYS[SUPER] @@ -86,7 +94,7 @@ def test_correct_key(qt_key, qt_mods, answer): """ qt_canvas = plt.figure().canvas - event = Mock() + event = mock.Mock() event.isAutoRepeat.return_value = False event.key.return_value = qt_key event.modifiers.return_value = qt_mods diff --git a/lib/matplotlib/tests/test_backend_qt5.py b/lib/matplotlib/tests/test_backend_qt5.py index df56b69a8791..478e2f2bbdde 100644 --- a/lib/matplotlib/tests/test_backend_qt5.py +++ b/lib/matplotlib/tests/test_backend_qt5.py @@ -7,13 +7,20 @@ import pytest -with matplotlib.rc_context(rc={'backend': 'Qt5Agg'}): - qt_compat = pytest.importorskip('matplotlib.backends.qt_compat', - minversion='5') +try: + import PyQt5 +except (ImportError, RuntimeError): # RuntimeError if PyQt4 already imported. + try: + import PySide2 + except ImportError: + pytestmark = pytest.mark.skip("Failed to import a Qt5 binding.") + +qt_compat = pytest.importorskip('matplotlib.backends.qt_compat') +QtCore = qt_compat.QtCore + from matplotlib.backends.backend_qt5 import ( MODIFIER_KEYS, SUPER, ALT, CTRL, SHIFT) # noqa -QtCore = qt_compat.QtCore _, ControlModifier, ControlKey = MODIFIER_KEYS[CTRL] _, AltModifier, AltKey = MODIFIER_KEYS[ALT] _, SuperModifier, SuperKey = MODIFIER_KEYS[SUPER] From 22508d8a455d48307665e4023acff5bb20b95485 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 6 Aug 2018 23:45:01 -0400 Subject: [PATCH 4/7] TST: pass force to `mpl.use` To actually make it change backends. This change is required because I reverted some of the more aggressive changes to `mpl.use` (to effectively default to force=True). --- lib/matplotlib/tests/test_backends_interactive.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 5652e18a525c..f7f520fd895b 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -71,10 +71,10 @@ def _get_testable_interactive_backends(): if backend != "tkagg": with assert_raises(ImportError): - mpl.use("tkagg") + mpl.use("tkagg", force=True) def check_alt_backend(alt_backend): - mpl.use(alt_backend) + mpl.use(alt_backend, force=True) fig = plt.figure() assert_equal( type(fig.canvas).__module__, @@ -84,7 +84,7 @@ def check_alt_backend(alt_backend): check_alt_backend(backend[:-3] + "cairo") check_alt_backend("svg") -mpl.use(backend) +mpl.use(backend, force=True) fig, ax = plt.subplots() assert_equal( From 0ccc19e5a1ec900a06ae2193a47bc68584317a0b Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 7 Aug 2018 22:08:22 -0400 Subject: [PATCH 5/7] TST: skip wx backend It seems to have a bug where `plt.show` hangs when the window is closed. --- lib/matplotlib/tests/test_backends_interactive.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index f7f520fd895b..d50bed1d3642 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -22,8 +22,9 @@ def _get_testable_interactive_backends(): (["cairocffi", "pgi"], "gtk3cairo"), (["PyQt5"], "qt5agg"), (["tkinter"], "tkagg"), - (["wx"], "wx"), - (["wx"], "wxagg")]: + # (["wx"], "wx"), + (["wx"], "wxagg") + ]: reason = None if not os.environ.get("DISPLAY"): reason = "No $DISPLAY" From 29dd9d42b330bfdd3eaf355d7402715c623d3aef Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 8 Aug 2018 16:21:00 -0400 Subject: [PATCH 6/7] TST: put back wx and qt5cairo tests --- lib/matplotlib/tests/test_backends_interactive.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index d50bed1d3642..10458dddf9cd 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -21,8 +21,9 @@ def _get_testable_interactive_backends(): for deps, backend in [ # (["cairocffi", "pgi"], "gtk3agg"), (["cairocffi", "pgi"], "gtk3cairo"), (["PyQt5"], "qt5agg"), + (["PyQt5", "cariocffi"], "qt5cairo"), (["tkinter"], "tkagg"), - # (["wx"], "wx"), + (["wx"], "wx"), (["wx"], "wxagg") ]: reason = None From 97683a58a3b0e94604ccc6380770f07e15c9c27f Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 10 Aug 2018 00:09:16 -0400 Subject: [PATCH 7/7] DOC: add whats new --- doc/users/whats_new.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/users/whats_new.rst b/doc/users/whats_new.rst index 00dade9beb59..4f89851070b5 100644 --- a/doc/users/whats_new.rst +++ b/doc/users/whats_new.rst @@ -228,6 +228,17 @@ This new method may be useful for adding artists to figures without axes or to easily position static elements in figure coordinates. +Improved default backend selection +---------------------------------- + +The default backend no longer must be set as part of the build +process. Instead, at run time, the builtin backends are tried in +sequence until one of them imports. + +Headless linux servers (identified by the DISPLAY env not being defined) +will not select a GUI backend. + + ================== Previous Whats New