Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6fc4b80

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 6fc4b80

File tree

8 files changed

+199
-86
lines changed

8 files changed

+199
-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", n, 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)