Skip to content

backend switching -- don't create a public fallback API #11600

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/api/next_api_changes/2018-02-15-AL-deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The following classes, methods, functions, and attributes are deprecated:
handle autorepeated key presses).
- ``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``,
Expand Down
11 changes: 11 additions & 0 deletions doc/users/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,17 @@ This new method may be useful for adding artists to figures without axes or to
easily position static elements in figure coordinates.


Improved default backend selection
----------------------------------

The default backend no longer must be set as part of the build
process. Instead, at run time, the builtin backends are tried in
sequence until one of them imports.

Headless linux servers (identified by the DISPLAY env not being defined)
will not select a GUI backend.



==================
Previous Whats New
Expand Down
99 changes: 44 additions & 55 deletions lib/matplotlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1301,74 +1301,63 @@ def __exit__(self, exc_type, exc_value, exc_tb):
dict.update(rcParams, self._orig)


_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,
or matplotlib.backends is imported for the first time.

The backend was *originally* set to {backend!r} by the following code:
{tb}
"""


def use(arg, warn=True, force=False):
"""
Set the matplotlib backend to one of the known backends.

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.
To find out which backend is currently set, see
:func:`matplotlib.get_backend`.


Parameters
----------
arg : str
The backend to switch to. This can either be one of the
'standard' backend names or a string of the form
``module://my.module.name``. This value is case-insensitive.

.. note::
warn : bool, optional
If True, warn if this is called after pyplot has been imported
and a backend is set up.

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.
defaults to True

force : bool, optional
If True, attempt to switch the backend. This defaults to
false and using `.pyplot.switch_backend` is preferred.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this docstring reference to 'switch_backend' was going to be deleted.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I missed that (but did remove it from warning message.


To find out which backend is currently set, see
:func:`matplotlib.get_backend`.

"""
# 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
name = validate_backend(arg)

# if setting back to the same thing, do nothing
if (rcParams['backend'] == name):
pass

# Check if we have already imported pyplot and triggered
# backend selection, do a bit more work
elif 'matplotlib.pyplot' in sys.modules:
# If we are here then the requested is different than the current.
# If we are going to force the switch, never warn, else, if warn
# is True, then direct users to `plt.switch_backend`
if (not force) and warn:
warnings.warn(
_use_error_msg.format(
backend=rcParams['backend'],
tb=matplotlib.backends._backend_loading_tb),
("matplotlib.pyplot as already been imported, "
"this call will have no effect."),
stacklevel=2)

# Unless we've been told to force it, just return
if not force:
return
need_reload = True
# if we are going to force switching the backend, pull in
# `switch_backend` from pyplot. This will only happen if
# pyplot is already imported.
if force:
from matplotlib.pyplot import switch_backend
switch_backend(name)
# Finally if pyplot is not imported update both rcParams and
# rcDefaults so restoring the defaults later with rcdefaults
# won't change the backend. This is a bit of overkill as 'backend'
# is already in style.core.STYLE_BLACKLIST, but better to be safe.
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'])
rcParams['backend'] = rcParamsDefault['backend'] = name


if os.environ.get('MPLBACKEND'):
Expand Down
7 changes: 6 additions & 1 deletion lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -3183,6 +3183,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"

Expand Down Expand Up @@ -3265,7 +3269,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",
Expand Down
6 changes: 3 additions & 3 deletions lib/matplotlib/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -54,16 +56,14 @@ def _get_running_interactive_framework():
except ImportError:
pass
else:
# Note that the NSApp event loop is also running when a non-native
# toolkit (e.g. Qt5) is active, but in that case we want to report the
# other toolkit; thus, this check comes after the other toolkits.
if _macosx.event_loop_is_running():
return "macosx"
if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"):
return "headless"
return None


@cbook.deprecated("3.0")
def pylab_setup(name=None):
"""
Return new_figure_manager, draw_if_interactive and show for pyplot.
Expand Down
75 changes: 60 additions & 15 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,7 +31,7 @@
import matplotlib
import matplotlib.colorbar
import matplotlib.image
from matplotlib import style
from matplotlib import rcsetup, style
from matplotlib import _pylab_helpers, interactive
from matplotlib.cbook import (
dedent, deprecated, silent_list, warn_deprecated, _string_to_bool)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -213,21 +216,60 @@ 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.

Calling this command will close all open windows.
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.

Parameters
----------
newbackend : str
The name of the backend to use.
"""
close('all')
close("all")

if newbackend is rcsetup._auto_backend_sentinel:
for candidate in ["macosx", "qt5agg", "qt4agg", "gtk3agg", "gtk3cairo",
"tkagg", "wxagg", "agg", "cairo"]:
try:
switch_backend(candidate)
except ImportError:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this catch the 'no display' errors you get on a headless server?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should(?), because _get_running_interactive_framework will return "headless" which is different from what the backend declares. We need the backend import to correctly fail with an ImportError, but I took care of the cases I encountered in the previous PR.

continue
else:
return

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'] = rcParamsDefault['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):
Expand Down Expand Up @@ -2364,6 +2406,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 ##############

Expand Down
15 changes: 8 additions & 7 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import operator
import os
import re
import sys

from matplotlib import cbook
from matplotlib.cbook import ls_mapper
Expand Down Expand Up @@ -242,13 +243,14 @@ def validate_fonttype(s):

_validate_standard_backends = ValidateInStrings(
'backend', all_backends, ignorecase=True)
_auto_backend_sentinel = object()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you may want to fix rcParams.__getitem__("backend") to check whether it would return the sentinel, and, if so, perform resolution and return an actual backend. That seems less bad (especially now that backend resolution can be reverted) than returning something which has no .lower() method... (why are backends case insensitive? that's an adventure for the next episode.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious as to why this used as the sentinel, instead of the more typical 'auto' or None.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we don't want to expose this as a public API (... at this point yet, at least).



def validate_backend(s):
if s.startswith('module://'):
return s
else:
return _validate_standard_backends(s)
backend = (
s if s is _auto_backend_sentinel or s.startswith("module://")
else _validate_standard_backends(s))
return backend


def validate_qt4(s):
Expand Down Expand Up @@ -965,9 +967,8 @@ 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': [_auto_backend_sentinel, validate_backend],
'backend_fallback': [True, validate_bool],
'backend.qt4': [None, validate_qt4],
'backend.qt5': [None, validate_qt5],
'webagg.port': [8988, validate_int],
Expand Down
Loading