Skip to content

Commit d018786

Browse files
committed
Derive new_figure_manager from FigureCanvas.new_manager.
Followup to the introduction of FigureCanvas.new_manager: allow backend modules to not define new_figure_manager anymore, in which case we derive the needed function from FigureCanvas.new_manager. (In the future, I plan to do the same with draw_if_interactive and show, so that "a backend is just a module with a FigureCanvas class"; the advantage is that the FigureCanvas subclass provided by the module can inherit methods as needed from the parent class.) For backcompat, the old codepath is maintained (and has priority). To test this, manually alter backend_bases._Backend.export and remove the new_figure_manager entry from the exported functions, which deletes that global function from all of the builtin methods (actually, we'll need a deprecation cycle), and check that pyplot still works fine. Also tweak the testing machinery to restore the original backend even if the backend was not switched via a pytest marker.
1 parent 7fba039 commit d018786

File tree

3 files changed

+58
-12
lines changed

3 files changed

+58
-12
lines changed

lib/matplotlib/pyplot.py

+33-9
Original file line numberDiff line numberDiff line change
@@ -266,15 +266,9 @@ def switch_backend(newbackend):
266266
rcParamsOrig["backend"] = "agg"
267267
return
268268

269-
# Backends are implemented as modules, but "inherit" default method
270-
# implementations from backend_bases._Backend. This is achieved by
271-
# creating a "class" that inherits from backend_bases._Backend and whose
272-
# body is filled with the module's globals.
273-
274-
backend_name = cbook._backend_module_name(newbackend)
275-
276-
class backend_mod(matplotlib.backend_bases._Backend):
277-
locals().update(vars(importlib.import_module(backend_name)))
269+
backend_mod = importlib.import_module(
270+
cbook._backend_module_name(newbackend))
271+
canvas_class = backend_mod.FigureCanvas
278272

279273
required_framework = _get_required_interactive_framework(backend_mod)
280274
if required_framework is not None:
@@ -286,6 +280,36 @@ class backend_mod(matplotlib.backend_bases._Backend):
286280
"framework, as {!r} is currently running".format(
287281
newbackend, required_framework, current_framework))
288282

283+
# Load the new_figure_manager(), draw_if_interactive(), and show()
284+
# functions from the backend.
285+
286+
# Classically, backends can directly export these functions. This should
287+
# keep working for backcompat.
288+
new_figure_manager = getattr(backend_mod, "new_figure_manager", None)
289+
# draw_if_interactive = getattr(backend_mod, "draw_if_interactive", None)
290+
# show = getattr(backend_mod, "show", None)
291+
# In that classical approach, backends are implemented as modules, but
292+
# "inherit" default method implementations from backend_bases._Backend.
293+
# This is achieved by creating a "class" that inherits from
294+
# backend_bases._Backend and whose body is filled with the module globals.
295+
class backend_mod(matplotlib.backend_bases._Backend):
296+
locals().update(vars(backend_mod))
297+
298+
# However, the newer approach for defining new_figure_manager (and, in
299+
# the future, draw_if_interactive and show) is to derive them from canvas
300+
# methods. In that case, also update backend_mod accordingly.
301+
if new_figure_manager is None:
302+
def new_figure_manager_given_figure(num, figure):
303+
return canvas_class.new_manager(figure, num)
304+
305+
def new_figure_manager(num, *args, FigureClass=Figure, **kwargs):
306+
fig = FigureClass(*args, **kwargs)
307+
return new_figure_manager_given_figure(num, fig)
308+
309+
backend_mod.new_figure_manager_given_figure = \
310+
new_figure_manager_given_figure
311+
backend_mod.new_figure_manager = new_figure_manager
312+
289313
_log.debug("Loaded backend %s version %s.",
290314
newbackend, backend_mod.backend_version)
291315

lib/matplotlib/testing/conftest.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ def mpl_test_settings(request):
4444

4545
backend = None
4646
backend_marker = request.node.get_closest_marker('backend')
47+
prev_backend = matplotlib.get_backend()
4748
if backend_marker is not None:
4849
assert len(backend_marker.args) == 1, \
4950
"Marker 'backend' must specify 1 backend."
5051
backend, = backend_marker.args
5152
skip_on_importerror = backend_marker.kwargs.get(
5253
'skip_on_importerror', False)
53-
prev_backend = matplotlib.get_backend()
5454

5555
# special case Qt backend importing to avoid conflicts
5656
if backend.lower().startswith('qt5'):
@@ -87,8 +87,7 @@ def mpl_test_settings(request):
8787
try:
8888
yield
8989
finally:
90-
if backend is not None:
91-
plt.switch_backend(prev_backend)
90+
matplotlib.use(prev_backend)
9291

9392

9493
@pytest.fixture
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
Backend-loading machinery tests, using variations on the template backend.
3+
"""
4+
5+
import sys
6+
from types import SimpleNamespace
7+
8+
import matplotlib as mpl
9+
from matplotlib import pyplot as plt
10+
from matplotlib.backends import backend_template
11+
12+
13+
def test_load_template():
14+
mpl.use("template")
15+
assert type(plt.figure().canvas) == backend_template.FigureCanvasTemplate
16+
17+
18+
def test_new_manager(monkeypatch):
19+
mpl_test_backend = SimpleNamespace(**vars(backend_template))
20+
del mpl_test_backend.new_figure_manager
21+
monkeypatch.setitem(sys.modules, "mpl_test_backend", mpl_test_backend)
22+
mpl.use("module://mpl_test_backend")
23+
assert type(plt.figure().canvas) == backend_template.FigureCanvasTemplate

0 commit comments

Comments
 (0)