-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
backend switching. #11581
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
backend switching. #11581
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -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 | ||
|
||
|
@@ -213,21 +216,61 @@ 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. | ||
|
||
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. | ||
|
||
Calling this command will close all open windows. | ||
Parameters | ||
---------- | ||
newbackend : str or List[str] | ||
The name of the backend to use. If a list of backends, they will be | ||
tried in order until one successfully loads. | ||
""" | ||
close('all') | ||
|
||
if not isinstance(newbackend, str): | ||
for candidate in newbackend: | ||
try: | ||
_log.info("Trying to load backend %s.", candidate) | ||
return switch_backend(candidate) | ||
except ImportError as exc: | ||
_log.info("Loading backend %s failed: %s", candidate, exc) | ||
else: | ||
raise ValueError("No suitable backend among {}".format(newbackend)) | ||
|
||
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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am worried that by this point we are already broken (due to importing the underlying GUI wrappers which may over-write each other's PyOS_EventHook), but I don't see any way around that short of delaying all of the imports (which I am not sure we can do). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PyOS_InputHook should (hopefully) not be overwritten before the toolkit event loop actually starts; e.g. for PyQt:
even after the QApplication is created, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But if you the import tk will the Qt windows still be responsive while waiting for user input? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes (this can easily be checked by, well, doing it). I'd guess tk also only sets up the input hook when actually starting the event loop. |
||
"Cannot load backend {!r} which requires the {!r} interactive " | ||
"framework, as {!r} is currently running".format( | ||
newbackend, required_framework, current_framework)) | ||
|
||
rcParams["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): | ||
|
@@ -2358,6 +2401,9 @@ def _autogen_docstring(base): | |
# to determine if they should trigger a draw. | ||
install_repl_displayhook() | ||
|
||
# Set up the backend. | ||
switch_backend(rcParams["backend"]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This step locks the backend in, @WeatherGod wants import matplotlib
matplotlib.use('qt5agg')
import matplotlib.pyplot
matplotlib.use('tkagg') # this does not warn or fail
plt.figure() # this makes a tk agg figure and actually does the backend import etc. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No it doesn't, because the QApplication isn't created until a figure is created. Indeed, your snippet works as expected. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am apparently confused about the details of various hooks get installed or not.... 🐑 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general nothing should happen until you create a widget; we then start the loop lazily. |
||
|
||
|
||
################# REMAINING CONTENT GENERATED BY boilerplate.py ############## | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,11 +13,13 @@ | |
parameter set listed here should also be visited to the | ||
:file:`matplotlibrc.template` in matplotlib's root source directory. | ||
""" | ||
|
||
from collections import Iterable, Mapping | ||
from functools import reduce | ||
import operator | ||
import os | ||
import re | ||
import sys | ||
|
||
from matplotlib import cbook | ||
from matplotlib.cbook import ls_mapper | ||
|
@@ -245,10 +247,35 @@ def validate_fonttype(s): | |
|
||
|
||
def validate_backend(s): | ||
if s.startswith('module://'): | ||
return s | ||
candidates = _listify_validator( | ||
lambda s: | ||
s if s.startswith("module://") | ||
else ValidateInStrings('backend', all_backends, ignorecase=True)(s))(s) | ||
pyplot = sys.modules.get("matplotlib.pyplot") | ||
if len(candidates) == 1: | ||
backend, = candidates | ||
if pyplot: | ||
# This import needs to be delayed (below too) because it is not | ||
# available at first import. | ||
from matplotlib import rcParams | ||
# Don't recurse. | ||
old_backend = rcParams["backend"] | ||
if old_backend == backend: | ||
return backend | ||
dict.__setitem__(rcParams, "backend", backend) | ||
try: | ||
pyplot.switch_backend(backend) | ||
except Exception: | ||
dict.__setitem__(rcParams, "backend", old_backend) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs to be done like this to make sure we un-wind any changes that `switch_backend_ makes to the rcparams? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, because switch_backend will set rcParams["backend"] too (to keep things consistent), but we don't want this to recurse ad infinitum (so we pre-set the entry -- but undo that if at the end of the day the backend switch was unsuccessful). Really it's just a complication because of the existence of two entry points (setting rcParams["backend"] and switch_backend) to the same info; it would even be simpler if switch_backend itself was inlined into the validator (but then you have funky action at distance with the proper initialization of pyplot happening in some other module...). |
||
raise | ||
return backend | ||
else: | ||
return _validate_standard_backends(s) | ||
if pyplot: | ||
from matplotlib import rcParams | ||
pyplot.switch_backend(candidates) # Actually resolves the backend. | ||
return rcParams["backend"] | ||
else: | ||
return candidates | ||
|
||
|
||
def validate_qt4(s): | ||
|
@@ -965,9 +992,13 @@ 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': [["macosx", | ||
"qt5agg", "qt4agg", | ||
"gtk3agg", "gtk3cairo", | ||
"tkagg", | ||
"wxagg", | ||
"agg", "cairo"], validate_backend], | ||
'backend_fallback': [True, validate_bool], | ||
'backend.qt4': [None, validate_qt4], | ||
'backend.qt5': [None, validate_qt5], | ||
'webagg.port': [8988, validate_int], | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By "
.pyplot
has already been imported" do you meanmatplotlib.use()
hasn't been called? Does pyplot fundamentally have anything to do with this, other than its where 99% of us callmatplotlib.use()
implicitly?