Skip to content

Commit 8915961

Browse files
committed
FIX: delay resolving automatic backend until actually needed
This is to prevent the early importing of GUI bindings in the case where the user wants to use something later in our search list, but also has a toolkit higher in our list installed which we select. Thanks to @anntzer for the implementation suggestion.
1 parent 5336d0e commit 8915961

File tree

4 files changed

+53
-15
lines changed

4 files changed

+53
-15
lines changed

lib/matplotlib/pyplot.py

+26-14
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ def _copy_docstring_and_deprecators(method, func=None):
104104

105105
## Global ##
106106

107-
108107
_IP_REGISTERED = None
109108
_INSTALL_FIG_OBSERVER = False
110109

@@ -207,6 +206,28 @@ def _get_required_interactive_framework(backend_mod):
207206
# Inline this once the deprecation elapses.
208207
return backend_mod.FigureCanvas.required_interactive_framework
209208

209+
_backend_mod = None
210+
211+
212+
def _get_backend_mod():
213+
"""
214+
Ensure that a backend is selected and return it.
215+
216+
This is currently private, but may be made public in the future.
217+
"""
218+
if _backend_mod is None:
219+
# Use __getitem__ here to avoid going through the fallback logic (which
220+
# will (re)import pyplot and then call switch_backend if we need to
221+
# resolve the auto sentinel)
222+
switch_backend(dict.__getitem__(rcParams, "backend"))
223+
# Just to be safe. Interactive mode can be turned on without calling
224+
# `plt.ion()` so register it again here. This is safe because multiple
225+
# calls to `install_repl_displayhook` are no-ops and the registered
226+
# function respects `mpl.is_interactive()` to determine if it should
227+
# trigger a draw.
228+
install_repl_displayhook()
229+
return _backend_mod
230+
210231

211232
def switch_backend(newbackend):
212233
"""
@@ -297,7 +318,7 @@ class backend_mod(matplotlib.backend_bases._Backend):
297318

298319

299320
def _warn_if_gui_out_of_main_thread():
300-
if (_get_required_interactive_framework(_backend_mod)
321+
if (_get_required_interactive_framework(_get_backend_mod())
301322
and threading.current_thread() is not threading.main_thread()):
302323
_api.warn_external(
303324
"Starting a Matplotlib GUI outside of the main thread will likely "
@@ -308,7 +329,7 @@ def _warn_if_gui_out_of_main_thread():
308329
def new_figure_manager(*args, **kwargs):
309330
"""Create a new figure manager instance."""
310331
_warn_if_gui_out_of_main_thread()
311-
return _backend_mod.new_figure_manager(*args, **kwargs)
332+
return _get_backend_mod().new_figure_manager(*args, **kwargs)
312333

313334

314335
# This function's signature is rewritten upon backend-load by switch_backend.
@@ -321,7 +342,7 @@ def draw_if_interactive(*args, **kwargs):
321342
End users will typically not have to call this function because the
322343
the interactive mode takes care of this.
323344
"""
324-
return _backend_mod.draw_if_interactive(*args, **kwargs)
345+
return _get_backend_mod().draw_if_interactive(*args, **kwargs)
325346

326347

327348
# This function's signature is rewritten upon backend-load by switch_backend.
@@ -370,7 +391,7 @@ def show(*args, **kwargs):
370391
explicitly there.
371392
"""
372393
_warn_if_gui_out_of_main_thread()
373-
return _backend_mod.show(*args, **kwargs)
394+
return _get_backend_mod().show(*args, **kwargs)
374395

375396

376397
def isinteractive():
@@ -2226,15 +2247,6 @@ def polar(*args, **kwargs):
22262247
set(_interactive_bk) - {'WebAgg', 'nbAgg'})
22272248
and cbook._get_running_interactive_framework()):
22282249
dict.__setitem__(rcParams, "backend", rcsetup._auto_backend_sentinel)
2229-
# Set up the backend.
2230-
switch_backend(rcParams["backend"])
2231-
2232-
# Just to be safe. Interactive mode can be turned on without
2233-
# calling `plt.ion()` so register it again here.
2234-
# This is safe because multiple calls to `install_repl_displayhook`
2235-
# are no-ops and the registered function respect `mpl.is_interactive()`
2236-
# to determine if they should trigger a draw.
2237-
install_repl_displayhook()
22382250

22392251

22402252
################# REMAINING CONTENT GENERATED BY boilerplate.py ##############

lib/matplotlib/tests/test_backend_tk.py

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def test_func():
6464
def test_blit(): # pragma: no cover
6565
import matplotlib.pyplot as plt
6666
import numpy as np
67+
import matplotlib.backends.backend_tkagg # noqa
6768
from matplotlib.backends import _tkagg
6869

6970
fig, ax = plt.subplots()

lib/matplotlib/tests/test_backends_interactive.py

+24
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import sys
1010
import time
1111
import urllib.request
12+
import textwrap
1213

1314
import pytest
1415

@@ -255,6 +256,29 @@ def test_interactive_thread_safety(env):
255256
assert proc.stdout.count("CloseEvent") == 1
256257

257258

259+
def test_lazy_auto_backend_selection():
260+
261+
def _impl():
262+
import matplotlib
263+
import matplotlib.pyplot as plt
264+
# just importing pyplot should not be enough to trigger resolution
265+
bk = dict.__getitem__(matplotlib.rcParams, 'backend')
266+
assert not isinstance(bk, str)
267+
assert plt._backend_mod is None
268+
# but actually plotting should
269+
plt.plot(5)
270+
assert plt._backend_mod is not None
271+
bk = dict.__getitem__(matplotlib.rcParams, 'backend')
272+
assert isinstance(bk, str)
273+
274+
proc = subprocess.run(
275+
[sys.executable, "-c",
276+
textwrap.dedent(inspect.getsource(_impl)) + "\n_impl()"],
277+
env={**os.environ, "SOURCE_DATE_EPOCH": "0"},
278+
timeout=_test_timeout, check=True,
279+
stdout=subprocess.PIPE, universal_newlines=True)
280+
281+
258282
@pytest.mark.skipif('TF_BUILD' in os.environ,
259283
reason="this test fails an azure for unknown reasons")
260284
@pytest.mark.skipif(os.name == "nt", reason="Cannot send SIGINT on Windows.")

lib/matplotlib/tests/test_rcparams.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,8 @@ def test_backend_fallback_headless(tmpdir):
497497
[sys.executable, "-c",
498498
"import matplotlib;"
499499
"matplotlib.use('tkagg');"
500-
"import matplotlib.pyplot"
500+
"import matplotlib.pyplot;"
501+
"matplotlib.pyplot.plot(42);"
501502
],
502503
env=env, check=True, stderr=subprocess.DEVNULL)
503504

0 commit comments

Comments
 (0)