8000 backend switching. · matplotlib/matplotlib@9c1403b · GitHub
[go: up one dir, main page]

Skip to content

Commit 9c1403b

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 7544c46 commit 9c1403b

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
@@ -1307,6 +1307,7 @@ def __exit__(self, exc_type, exc_value, exc_tb):
13071307
dict.update(rcParams, self._orig)
13081308

13091309

1310+
# FIXME: Remove.
13101311
_use_error_msg = """
13111312
This call to matplotlib.use() has no effect because the backend has already
13121313
been chosen; matplotlib.use() must be called *before* pylab, matplotlib.pyplot,
@@ -1319,62 +1320,26 @@ def __exit__(self, exc_type, exc_value, exc_tb):
13191320

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

13791344

13801345
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):
@@ -2348,6 +2391,9 @@ def _autogen_docstring(base):
23482391
# to determine if they should trigger a draw.
23492392
install_repl_displayhook()
23502393

2394+
# Set up the backend.
2395+
switch_backend(rcParams["backend"])
2396+
23512397

23522398
################# REMAINING CONTENT GENERATED BY boilerplate.py ##############
23532399

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)
0