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 df34bd47dab4..4e56518ada14 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 @@ -18,6 +18,7 @@ The following classes, methods, functions, and attributes are deprecated: - ``backend_ps.get_bbox``, - ``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/doc/api/next_api_changes/2018-06-27-AL.rst b/doc/api/next_api_changes/2018-06-27-AL.rst index 51a8171bf02d..3a350e91b1ff 100644 --- a/doc/api/next_api_changes/2018-06-27-AL.rst +++ b/doc/api/next_api_changes/2018-06-27-AL.rst @@ -1,6 +1,28 @@ Changes to backend loading `````````````````````````` +It is now possible to set ``rcParams["backend"]`` to a *list* of candidate +backends. + +If `.pyplot` has already been imported, Matplotlib will try to load each +candidate backend in the given order until one of them can be loaded +successfully. ``rcParams["backend"]`` will then be set to the value of the +successfully loaded backend. (If `.pyplot` has already been imported and +``rcParams["backend"]`` is set to a single value, then the backend will +likewise be updated.) + +If `.pyplot` has not been imported yet, then ``rcParams["backend"]`` will +maintain the value as a list, and the loading attempt will occur when `.pyplot` +is imported. If you rely on ``rcParams["backend"]`` (or its synonym, +``matplotlib.get_backend()`` always being a string, import `.pyplot` to trigger +backend resolution. + +`.pyplot.switch_backends` (but not `matplotlib.use`) have likewise gained the +ability to accept a list of candidate backends. + +In order to support the above features, the additional following changes were +made: + Failure to load backend modules (``macosx`` on non-framework builds and ``gtk3`` when running headless) now raises `ImportError` (instead of `RuntimeError` and `TypeError`, respectively. diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 57b69ed30cd7..59dd081d2c1b 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1307,6 +1307,7 @@ def __exit__(self, exc_type, exc_value, exc_tb): dict.update(rcParams, self._orig) +# FIXME: Remove. _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, @@ -1319,62 +1320,26 @@ def __exit__(self, exc_type, exc_value, exc_tb): def use(arg, warn=True, force=False): """ - Set the matplotlib backend to one of the known backends. + Set the Matplotlib backend. - 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. + The argument is case-insensitive. Switching to an interactive backend is + only safe if no event loop for another interactive backend has started. + Switching to and from non-interactive backends is safe. - .. note:: - - 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. - - To find out which backend is currently set, see - :func:`matplotlib.get_backend`. + To find out which backend is currently set, see `matplotlib.get_backend`. + Parameters + ---------- + arg : str + The name of the backend to use. """ - # 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 - warnings.warn( - _use_error_msg.format( - backend=rcParams['backend'], - tb=matplotlib.backends._backend_loading_tb), - stacklevel=2) - - # Unless we've been told to force it, just return - if not force: - return - need_reload = True - 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']) + if not isinstance(arg, str): + # We want to keep 'use(...); rcdefaults()' working, which means that + # use(...) needs to force the default backend, and thus be a single + # string. + raise TypeError("matplotlib.use takes a single string as argument") + rcParams["backend"] = \ + rcParamsDefault["backend"] = rcParamsOrig["backend"] = arg if os.environ.get('MPLBACKEND'): diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index a194b985b587..09b621f53489 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3210,6 +3210,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" @@ -3292,7 +3296,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 045b56c0e076..278b3783188e 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 @@ -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,61 @@ 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. + + 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. - Calling this command will close all open windows. + Parameters + ---------- + newbackend : str or List[str] + The name of the backend to use. If a list of backends, they will be + tried in order until one successfully loads. """ close('all') + + if not isinstance(newbackend, str): + for candidate in newbackend: + try: + _log.info("Trying to load backend %s.", candidate) + return switch_backend(candidate) + except ImportError as exc: + _log.info("Loading backend %s failed: %s", n, exc) + else: + raise ValueError("No suitable backend among {}".format(newbackend)) + + 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"] = 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): @@ -2348,6 +2391,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 594154b5e088..965a23da746b 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -13,11 +13,13 @@ parameter set listed here should also be visited to the :file:`matplotlibrc.template` in matplotlib's root source directory. """ + from collections import Iterable, Mapping from functools import reduce import operator import os import re +import sys from matplotlib import cbook from matplotlib.cbook import ls_mapper @@ -245,10 +247,35 @@ def validate_fonttype(s): def validate_backend(s): - if s.startswith('module://'): - return s + candidates = _listify_validator( + lambda s: + s if s.startswith("module://") + else ValidateInStrings('backend', all_backends, ignorecase=True)(s))(s) + pyplot = sys.modules.get("matplotlib.pyplot") + if len(candidates) == 1: + backend, = candidates + if pyplot: + # This import needs to be delayed (below too) because it is not + # available at first import. + from matplotlib import rcParams + # Don't recurse. + old_backend = rcParams["backend"] + if old_backend == backend: + return backend + dict.__setitem__(rcParams, "backend", backend) + try: + pyplot.switch_backend(backend) + except Exception: + dict.__setitem__(rcParams, "backend", old_backend) + raise + return backend else: - return _validate_standard_backends(s) + if pyplot: + from matplotlib import rcParams + pyplot.switch_backend(candidates) # Actually resolves the backend. + return rcParams["backend"] + else: + return candidates def validate_qt4(s): @@ -965,9 +992,13 @@ 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': [["macosx", + "qt5agg", "qt4agg", + "gtk3agg", "gtk3cairo", + "tkagg", + "wxagg", + "agg", "cairo"], 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 0d4d41420839..6916ccc23663 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -18,9 +18,7 @@ def _get_testable_interactive_backends(): backends = [] for deps, backend in [(["cairocffi", "pgi"], "gtk3agg"), - (["cairocffi", "pgi"], "gtk3cairo"), (["PyQt5"], "qt5agg"), - (["cairocffi", "PyQt5"], "qt5cairo"), (["tkinter"], "tkagg"), (["wx"], "wx"), (["wx"], "wxagg")]: @@ -29,33 +27,75 @@ def _get_testable_interactive_backends(): reason = "No $DISPLAY" elif any(importlib.util.find_spec(dep) is None for dep in deps): reason = "Missing dependency" + # Ugh, vext makes module specs findable even if they cannot be + # imported. backends.append(pytest.mark.skip(reason=reason)(backend) if reason else backend) return backends -# 1. Using a timer not only allows testing of timers (on other backends), but -# is also necessary on gtk3 and wx, where a direct call to -# key_press_event("q") from draw_event causes breakage due to the canvas -# widget being deleted too early. -# 2. On gtk3, we cannot even test the timer setup (on Travis, which uses pgi) -# due to https://github.com/pygobject/pgi/issues/45. So we just cleanly -# exit from the draw_event. _test_script = """\ +import importlib import sys +from unittest import TestCase + +import matplotlib as mpl from matplotlib import pyplot as plt, rcParams + 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"): + # 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. + + 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 = plt.figure() +assert_equal( + type(fig.canvas).__module__, + "matplotlib.backends.backend_{}".format(backend)) + ax = fig.add_subplot(111) ax.plot([0, 1], [2, 3]) -if rcParams["backend"].startswith("GTK3"): +if backend.startswith("gtk3"): + # On gtk3, we cannot even test the timer setup (on Travis, which uses pgi) + # due to https://github.com/pygobject/pgi/issues/45. So we just cleanly + # exit from the draw_event. fig.canvas.mpl_connect("draw_event", lambda event: sys.exit(0)) else: + # Using a timer not only allows testing of timers (on other backends), but + # is also necessary on gtk3 and wx, where a direct call to + # key_press_event("q") from draw_event causes breakage due to the canvas + # widget being deleted too early. timer = fig.canvas.new_timer(1) timer.add_callback(fig.canvas.key_press_event, "q") # Trigger quitting upon draw. @@ -69,10 +109,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.")