Skip to content

Commit e8f0b22

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.
1 parent 4fc9288 commit e8f0b22

File tree

8 files changed

+152
-89
lines changed

8 files changed

+152
-89
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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
Changes to backend loading
22
``````````````````````````
33

4+
Assignment to ``rcParams["backend"]`` now sets the backend. Backends can now
5+
be switched until a figure is actually created.
6+
47
Failure to load backend modules (``macosx`` on non-framework builds and
58
``gtk3`` when running headless) now raises `ImportError` (instead of
69
`RuntimeError` and `TypeError`, respectively.

lib/matplotlib/__init__.py

Lines changed: 14 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,23 @@ 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+
# We want to keep 'use(...); rcdefaults()' working, which means that
1347+
# use(...) needs to force the default backend too.
1348+
rcParams["backend"] = \
1349+
rcParamsDefault["backend"] = rcParamsOrig["backend"] = arg
13881350

13891351

13901352
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: 60 additions & 15 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
@@ -29,7 +31,7 @@
2931
import matplotlib
3032
import matplotlib.colorbar
3133
import matplotlib.image
32-
from matplotlib import style
34+
from matplotlib import rcsetup, style
3335
from matplotlib import _pylab_helpers, interactive
3436
from matplotlib.cbook import (
3537
dedent, deprecated, silent_list, warn_deprecated, _string_to_bool)
@@ -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,60 @@ 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.
223220
224-
Calling this command will close all open windows.
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.
224+
225+
Parameters
226+
----------
227+
newbackend : str
228+
The name of the backend to use.
225229
"""
226-
close('all')
230+
close("all")
231+
232+
if newbackend is rcsetup._auto_backend_sentinel:
233+
for candidate in ["macosx", "qt5agg", "qt4agg", "gtk3agg", "gtk3cairo",
234+
"tkagg", "wxagg", "agg", "cairo"]:
235+
try:
236+
switch_backend(candidate)
237+
except ImportError:
238+
continue
239+
else:
240+
return
241+
242+
backend_name = (
243+
newbackend[9:] if newbackend.startswith("module://")
244+
else "matplotlib.backends.backend_{}".format(newbackend.lower()))
245+
246+
backend_mod = importlib.import_module(backend_name)
247+
Backend = type(
248+
"Backend", (matplotlib.backends._Backend,), vars(backend_mod))
249+
_log.info("Loaded backend %s version %s.",
250+
newbackend, Backend.backend_version)
251+
252+
required_framework = Backend.required_interactive_framework
253+
current_framework = \
254+
matplotlib.backends._get_running_interactive_framework()
255+
if (current_framework and required_framework
256+
and current_framework != required_framework):
257+
raise ImportError(
258+
"Cannot load backend {!r} which requires the {!r} interactive "
259+
"framework, as {!r} is currently running".format(
260+
newbackend, required_framework, current_framework))
261+
262+
dict.__setitem__(rcParams, "backend", newbackend) # Don't recurse.
263+
227264
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()
265+
_backend_mod = backend_mod
266+
new_figure_manager = Backend.new_figure_manager
267+
draw_if_interactive = Backend.draw_if_interactive
268+
_show = Backend.show
269+
270+
# Need to keep a global reference to the backend for compatibility reasons.
271+
# See https://github.com/matplotlib/matplotlib/issues/6092
272+
matplotlib.backends.backend = newbackend
231273

232274

233275
def show(*args, **kw):
@@ -2358,6 +2400,9 @@ def _autogen_docstring(base):
23582400
# to determine if they should trigger a draw.
23592401
install_repl_displayhook()
23602402

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

23622407
################# REMAINING CONTENT GENERATED BY boilerplate.py ##############
23632408

lib/matplotlib/rcsetup.py

Lines changed: 12 additions & 7 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
@@ -242,13 +244,17 @@ def validate_fonttype(s):
242244

243245
_validate_standard_backends = ValidateInStrings(
244246
'backend', all_backends, ignorecase=True)
247+
_auto_backend_sentinel = object()
245248

246249

247250
def validate_backend(s):
248-
if s.startswith('module://'):
249-
return s
250-
else:
251-
return _validate_standard_backends(s)
251+
backend = (
252+
s if s is _auto_backend_sentinel or s.startswith("module://")
253+
else _validate_standard_backends(s))
254+
pyplot = sys.modules.get("matplotlib.pyplot")
255+
if pyplot:
256+
pyplot.switch_backend(backend)
257+
return backend
252258

253259

254260
def validate_qt4(s):
@@ -965,9 +971,8 @@ def _validate_linestyle(ls):
965971

966972
# a map from key -> value, converter
967973
defaultParams = {
968-
'backend': ['Agg', validate_backend], # agg is certainly
969-
# present
970-
'backend_fallback': [True, validate_bool], # agg is certainly present
974+
'backend': [_auto_backend_sentinel, validate_backend],
975+
'backend_fallback': [True, validate_bool],
971976
'backend.qt4': [None, validate_qt4],
972977
'backend.qt5': [None, validate_qt5],
973978
'webagg.port': [8988, validate_int],

0 commit comments

Comments
 (0)