Skip to content

Commit b768f10

Browse files
committed
backend switching.
See changes documented in the API changes file. Some followup cleanup (of the now unused old machinery) will come as a separate PR (left some "FIXME: Remove." comments). Changes to the build process (namely, getting rid of trying to detect the default backend in setupext.py) will come as a separate PR. I inlined pylab_setup into switch_backend (and deprecated the old version of pylab_setup) because otherwise the typical call stack would be `use()` -> `set rcParams['backend'] = ...` -> `switch_backend()` -> `pylab_setup()`, which is a bit of a mess; at least we can get rid of one of the layers. If the API change ("rcParams['backend'] returns a list as long as pyplot has not been imported") is deemed unacceptable, we could also make *reading* rcParams["backend"] force backend resolution (by hooking `__getattr__`).
1 parent 4fc9288 commit b768f10

File tree

8 files changed

+198
-86
lines changed

8 files changed

+198
-86
lines changed

doc/api/next_api_changes/2018-02-15-AL-deprecations.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ The following classes, methods, functions, and attributes are deprecated:
1818
- ``backend_ps.get_bbox``,
1919
- ``backend_qt5.error_msg_qt``, ``backend_qt5.exception_handler``,
2020
- ``backend_wx.FigureCanvasWx.macros``,
21+
- ``backends.pylab_setup``,
2122
- ``cbook.GetRealpathAndStat``, ``cbook.Locked``,
2223
- ``cbook.is_numlike`` (use ``isinstance(..., numbers.Number)`` instead),
2324
``cbook.listFiles``, ``cbook.unicode_safe``,

doc/api/next_api_changes/2018-06-27-AL.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,28 @@
11
Changes to backend loading
22
``````````````````````````
33

4+
It is now possible to set ``rcParams["backend"]`` to a *list* of candidate
5+
backends.
6+
7+
If `.pyplot` has already been imported, Matplotlib will try to load each
8+
candidate backend in the given order until one of them can be loaded
9+
successfully. ``rcParams["backend"]`` will then be set to the value of the
10+
successfully loaded backend. (If `.pyplot` has already been imported and
11+
``rcParams["backend"]`` is set to a single value, then the backend will
12+
likewise be updated.)
13+
14+
If `.pyplot` has not been imported yet, then ``rcParams["backend"]`` will
15+
maintain the value as a list, and the loading attempt will occur when `.pyplot`
16+
is imported. If you rely on ``rcParams["backend"]`` (or its synonym,
17+
``matplotlib.get_backend()`` always being a string, import `.pyplot` to trigger
18+
backend resolution.
19+
20+
`.pyplot.switch_backends` (but not `matplotlib.use`) have likewise gained the
21+
ability to accept a list of candidate backends.
22+
23+
In order to support the above features, the additional following changes were
24+
made:
25+
426
Failure to load backend modules (``macosx`` on non-framework builds and
527
``gtk3`` when running headless) now raises `ImportError` (instead of
628
`RuntimeError` and `TypeError`, respectively.

lib/matplotlib/__init__.py

Lines changed: 17 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,6 +1317,7 @@ def __exit__(self, exc_type, exc_value, exc_tb):
13171317
dict.update(rcParams, self._orig)
13181318

13191319

1320+
# FIXME: Remove.
13201321
_use_error_msg = """
13211322
This call to matplotlib.use() has no effect because the backend has already
13221323
been chosen; matplotlib.use() must be called *before* pylab, matplotlib.pyplot,
@@ -1329,62 +1330,26 @@ def __exit__(self, exc_type, exc_value, exc_tb):
13291330

13301331
def use(arg, warn=True, force=False):
13311332
"""
1332-
Set the matplotlib backend to one of the known backends.
1333+
Set the Matplotlib backend.
13331334
1334-
The argument is case-insensitive. *warn* specifies whether a
1335-
warning should be issued if a backend has already been set up.
1336-
*force* is an **experimental** flag that tells matplotlib to
1337-
attempt to initialize a new backend by reloading the backend
1338-
module.
1335+
The argument is case-insensitive. Switching to an interactive backend is
1336+
only safe if no event loop for another interactive backend has started.
1337+
Switching to and from non-interactive backends is safe.
13391338
1340-
.. note::
1341-
1342-
This function must be called *before* importing pyplot for
1343-
the first time; or, if you are not using pyplot, it must be called
1344-
before importing matplotlib.backends. If warn is True, a warning
1345-
is issued if you try and call this after pylab or pyplot have been
1346-
loaded. In certain black magic use cases, e.g.
1347-
:func:`pyplot.switch_backend`, we are doing the reloading necessary to
1348-
make the backend switch work (in some cases, e.g., pure image
1349-
backends) so one can set warn=False to suppress the warnings.
1350-
1351-
To find out which backend is currently set, see
1352-
:func:`matplotlib.get_backend`.
1339+
To find out which backend is currently set, see `matplotlib.get_backend`.
13531340
1341+
Parameters
1342+
----------
1343+
arg : str
1344+
The name of the backend to use.
13541345
"""
1355-
# Lets determine the proper backend name first
1356-
if arg.startswith('module://'):
1357-
name = arg
1358-
else:
1359-
# Lowercase only non-module backend names (modules are case-sensitive)
1360-
arg = arg.lower()
1361-
name = validate_backend(arg)
1362-
1363-
# Check if we've already set up a backend
1364-
if 'matplotlib.backends' in sys.modules:
1365-
# Warn only if called with a different name
1366-
if (rcParams['backend'] != name) and warn:
1367-
import matplotlib.backends
1368-
warnings.warn(
1369-
_use_error_msg.format(
1370-
backend=rcParams['backend'],
1371-
tb=matplotlib.backends._backend_loading_tb),
1372-
stacklevel=2)
1373-
1374-
# Unless we've been told to force it, just return
1375-
if not force:
1376-
return
1377-
need_reload = True
1378-
else:
1379-
need_reload = False
1380-
1381-
# Store the backend name
1382-
rcParams['backend'] = name
1383-
1384-
# If needed we reload here because a lot of setup code is triggered on
1385-
# module import. See backends/__init__.py for more detail.
1386-
if need_reload:
1387-
importlib.reload(sys.modules['matplotlib.backends'])
1346+
if not isinstance(arg, str):
1347+
# We want to keep 'use(...); rcdefaults()' working, which means that
1348+
# use(...) needs to force the default backend, and thus be a single
1349+
# string.
1350+
raise TypeError("matplotlib.use takes a single string as argument")
1351+
rcParams["backend"] = \
1352+
rcParamsDefault["backend"] = rcParamsOrig["backend"] = arg
13881353

13891354

13901355
if os.environ.get('MPLBACKEND'):

lib/matplotlib/backend_bases.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3210,6 +3210,10 @@ class _Backend(object):
32103210
# class FooBackend(_Backend):
32113211
# # override the attributes and methods documented below.
32123212

3213+
# Set to one of {"qt5", "qt4", "gtk3", "wx", "tk", "macosx"} if an
3214+
# interactive framework is required, or None otherwise.
3215+
required_interactive_framework = None
3216+
32133217
# `backend_version` may be overridden by the subclass.
32143218
backend_version = "unknown"
32153219

@@ -3292,7 +3296,8 @@ def show(cls, block=None):
32923296

32933297
@staticmethod
32943298
def export(cls):
3295-
for name in ["backend_version",
3299+
for name in ["required_interactive_framework",
3300+
"backend_version",
32963301
"FigureCanvas",
32973302
"FigureManager",
32983303
"new_figure_manager",

lib/matplotlib/backends/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
import traceback
66

77
import matplotlib
8+
from matplotlib import cbook
89
from matplotlib.backend_bases import _Backend
910

1011
_log = logging.getLogger(__name__)
1112

1213
backend = matplotlib.get_backend()
14+
# FIXME: Remove.
1315
_backend_loading_tb = "".join(
1416
line for line in traceback.format_stack()
1517
# Filter out line noise from importlib line.
@@ -64,6 +66,7 @@ def _get_running_interactive_framework():
6466
return None
6567

6668

69+
@cbook.deprecated("3.0")
6770
def pylab_setup(name=None):
6871
"""
6972
Return new_figure_manager, draw_if_interactive and show for pyplot.

lib/matplotlib/pyplot.py

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
The object-oriented API is recommended for more complex plots.
1919
"""
2020

21+
import importlib
2122
import inspect
23+
import logging
2224
from numbers import Number
2325
import re
2426
import sys
@@ -67,10 +69,13 @@
6769
MaxNLocator
6870
from matplotlib.backends import pylab_setup
6971

72+
_log = logging.getLogger(__name__)
73+
7074

7175
## Backend detection ##
7276

7377

78+
# FIXME: Deprecate.
7479
def _backend_selection():
7580
"""
7681
If rcParams['backend_fallback'] is true, check to see if the
@@ -110,8 +115,6 @@ def _backend_selection():
110115
## Global ##
111116

112117

113-
_backend_mod, new_figure_manager, draw_if_interactive, _show = pylab_setup()
114-
115118
_IP_REGISTERED = None
116119
_INSTALL_FIG_OBSERVER = False
117120

@@ -213,21 +216,61 @@ def findobj(o=None, match=None, include_self=True):
213216

214217
def switch_backend(newbackend):
215218
"""
216-
Switch the default backend. This feature is **experimental**, and
217-
is only expected to work switching to an image backend. e.g., if
218-
you have a bunch of PostScript scripts that you want to run from
219-
an interactive ipython session, you may want to switch to the PS
220-
backend before running them to avoid having a bunch of GUI windows
221-
popup. If you try to interactively switch from one GUI backend to
222-
another, you will explode.
219+
Close all open figures and set the Matplotlib backend.
220+
221+
The argument is case-insensitive. Switching to an interactive backend is
222+
possible only if no event loop for another interactive backend has started.
223+
Switching to and from non-interactive backends is always possible.
223224
224-
Calling this command will close all open windows.
225+
Parameters
226+
----------
227+
newbackend : str or List[str]
228+
The name of the backend to use. If a list of backends, they will be
229+
tried in order until one successfully loads.
225230
"""
226231
close('all')
232+
233+
if not isinstance(newbackend, str):
234+
for candidate in newbackend:
235+
try:
236+
_log.info("Trying to load backend %s.", candidate)
237+
return switch_backend(candidate)
238+
except ImportError as exc:
239+
_log.info("Loading backend %s failed: %s", candidate, exc)
240+
else:
241+
raise ValueError("No suitable backend among {}".format(newbackend))
242+
243+
backend_name = (
244+
newbackend[9:] if newbackend.startswith("module://")
245+
else "matplotlib.backends.backend_{}".format(newbackend.lower()))
246+
247+
backend_mod = importlib.import_module(backend_name)
248+
Backend = type(
249+
"Backend", (matplotlib.backends._Backend,), vars(backend_mod))
250+
_log.info("Loaded backend %s version %s.",
251+
newbackend, Backend.backend_version)
252+
253+
required_framework = Backend.required_interactive_framework
254+
current_framework = \
255+
matplotlib.backends._get_running_interactive_framework()
256+
if (current_framework and required_framework
257+
and current_framework != required_framework):
258+
raise ImportError(
259+
"Cannot load backend {!r} which requires the {!r} interactive "
260+
"framework, as {!r} is currently running".format(
261+
newbackend, required_framework, current_framework))
262+
263+
rcParams["backend"] = newbackend
264+
227265
global _backend_mod, new_figure_manager, draw_if_interactive, _show
228-
matplotlib.use(newbackend, warn=False, force=True)
229-
from matplotlib.backends import pylab_setup
230-
_backend_mod, new_figure_manager, draw_if_interactive, _show = pylab_setup()
266+
_backend_mod = backend_mod
267+
new_figure_manager = Backend.new_figure_manager
268+
draw_if_interactive = Backend.draw_if_interactive
269+
_show = Backend.show
270+
271+
# Need to keep a global reference to the backend for compatibility reasons.
272+
# See https://github.com/matplotlib/matplotlib/issues/6092
273+
matplotlib.backends.backend = newbackend
231274

232275

233276
def show(*args, **kw):
@@ -2358,6 +2401,9 @@ def _autogen_docstring(base):
23582401
# to determine if they should trigger a draw.
23592402
install_repl_displayhook()
23602403

2404+
# Set up the backend.
2405+
switch_backend(rcParams["backend"])
2406+
23612407

23622408
################# REMAINING CONTENT GENERATED BY boilerplate.py ##############
23632409

lib/matplotlib/rcsetup.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
parameter set listed here should also be visited to the
1414
:file:`matplotlibrc.template` in matplotlib's root source directory.
1515
"""
16+
1617
from collections import Iterable, Mapping
1718
from functools import reduce
1819
import operator
1920
import os
2021
import re
22+
import sys
2123

2224
from matplotlib import cbook
2325
from matplotlib.cbook import ls_mapper
@@ -245,10 +247,35 @@ def validate_fonttype(s):
245247

246248

247249
def validate_backend(s):
248-
if s.startswith('module://'):
249-
return s
250+
candidates = _listify_validator(
251+
lambda s:
252+
s if s.startswith("module://")
253+
else ValidateInStrings('backend', all_backends, ignorecase=True)(s))(s)
254+
pyplot = sys.modules.get("matplotlib.pyplot")
255+
if len(candidates) == 1:
256+
backend, = candidates
257+
if pyplot:
258+
# This import needs to be delayed (below too) because it is not
259+
# available at first import.
260+
from matplotlib import rcParams
261+
# Don't recurse.
262+
old_backend = rcParams["backend"]
263+
if old_backend == backend:
264+
return backend
265+
dict.__setitem__(rcParams, "backend", backend)
266+
try:
267+
pyplot.switch_backend(backend)
268+
except Exception:
269+
dict.__setitem__(rcParams, "backend", old_backend)
270+
raise
271+
return backend
250272
else:
251-
return _validate_standard_backends(s)
273+
if pyplot:
274+
from matplotlib import rcParams
275+
pyplot.switch_backend(candidates) # Actually resolves the backend.
276+
return rcParams["backend"]
277+
else:
278+
return candidates
252279

253280

254281
def validate_qt4(s):
@@ -965,9 +992,13 @@ def _validate_linestyle(ls):
965992

966993
# a map from key -> value, converter
967994
defaultParams = {
968-
'backend': ['Agg', validate_backend], # agg is certainly
969-
# present
970-
'backend_fallback': [True, validate_bool], # agg is certainly present
995+
'backend': [["macosx",
996+
"qt5agg", "qt4agg",
997+
"gtk3agg", "gtk3cairo",
998+
"tkagg",
999+
"wxagg",
1000+
"agg", "cairo"], validate_backend],
1001+
'backend_fallback': [True, validate_bool],
9711002
'backend.qt4': [None, validate_qt4],
9721003
'backend.qt5': [None, validate_qt5],
9731004
'webagg.port': [8988, validate_int],

0 commit comments

Comments
 (0)