From d018786d9ab532edf55c1b1b241de95b5f89a9a3 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 21 May 2022 20:22:23 +0200 Subject: [PATCH] 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. --- lib/matplotlib/pyplot.py | 42 +++++++++++++++---- lib/matplotlib/testing/conftest.py | 5 +-- lib/matplotlib/tests/test_backend_template.py | 23 ++++++++++ 3 files changed, 58 insertions(+), 12 deletions(-) create mode 100644 lib/matplotlib/tests/test_backend_template.py diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 7a398b25975b..6b6653de83e8 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -266,15 +266,9 @@ def switch_backend(newbackend): rcParamsOrig["backend"] = "agg" return - # Backends are implemented as modules, but "inherit" default method - # implementations from backend_bases._Backend. This is achieved by - # creating a "class" that inherits from backend_bases._Backend and whose - # body is filled with the module's globals. - - backend_name = cbook._backend_module_name(newbackend) - - class backend_mod(matplotlib.backend_bases._Backend): - locals().update(vars(importlib.import_module(backend_name))) + backend_mod = importlib.import_module( + cbook._backend_module_name(newbackend)) + canvas_class = backend_mod.FigureCanvas required_framework = _get_required_interactive_framework(backend_mod) if required_framework is not None: @@ -286,6 +280,36 @@ class backend_mod(matplotlib.backend_bases._Backend): "framework, as {!r} is currently running".format( newbackend, required_framework, current_framework)) + # Load the new_figure_manager(), draw_if_interactive(), and show() + # functions from the backend. + + # Classically, backends can directly export these functions. This should + # keep working for backcompat. + new_figure_manager = getattr(backend_mod, "new_figure_manager", None) + # draw_if_interactive = getattr(backend_mod, "draw_if_interactive", None) + # show = getattr(backend_mod, "show", None) + # In that classical approach, backends are implemented as modules, but + # "inherit" default method implementations from backend_bases._Backend. + # This is achieved by creating a "class" that inherits from + # backend_bases._Backend and whose body is filled with the module globals. + class backend_mod(matplotlib.backend_bases._Backend): + locals().update(vars(backend_mod)) + + # However, the newer approach for defining new_figure_manager (and, in + # the future, draw_if_interactive and show) is to derive them from canvas + # methods. In that case, also update backend_mod accordingly. + if new_figure_manager is None: + def new_figure_manager_given_figure(num, figure): + return canvas_class.new_manager(figure, num) + + def new_figure_manager(num, *args, FigureClass=Figure, **kwargs): + fig = FigureClass(*args, **kwargs) + return new_figure_manager_given_figure(num, fig) + + backend_mod.new_figure_manager_given_figure = \ + new_figure_manager_given_figure + backend_mod.new_figure_manager = new_figure_manager + _log.debug("Loaded backend %s version %s.", newbackend, backend_mod.backend_version) diff --git a/lib/matplotlib/testing/conftest.py b/lib/matplotlib/testing/conftest.py index 01e60fea05e4..d9c4f17e1b27 100644 --- a/lib/matplotlib/testing/conftest.py +++ b/lib/matplotlib/testing/conftest.py @@ -44,13 +44,13 @@ def mpl_test_settings(request): backend = None backend_marker = request.node.get_closest_marker('backend') + prev_backend = matplotlib.get_backend() if backend_marker is not None: assert len(backend_marker.args) == 1, \ "Marker 'backend' must specify 1 backend." backend, = backend_marker.args skip_on_importerror = backend_marker.kwargs.get( 'skip_on_importerror', False) - prev_backend = matplotlib.get_backend() # special case Qt backend importing to avoid conflicts if backend.lower().startswith('qt5'): @@ -87,8 +87,7 @@ def mpl_test_settings(request): try: yield finally: - if backend is not None: - plt.switch_backend(prev_backend) + matplotlib.use(prev_backend) @pytest.fixture diff --git a/lib/matplotlib/tests/test_backend_template.py b/lib/matplotlib/tests/test_backend_template.py new file mode 100644 index 000000000000..31ab644f248f --- /dev/null +++ b/lib/matplotlib/tests/test_backend_template.py @@ -0,0 +1,23 @@ +""" +Backend-loading machinery tests, using variations on the template backend. +""" + +import sys +from types import SimpleNamespace + +import matplotlib as mpl +from matplotlib import pyplot as plt +from matplotlib.backends import backend_template + + +def test_load_template(): + mpl.use("template") + assert type(plt.figure().canvas) == backend_template.FigureCanvasTemplate + + +def test_new_manager(monkeypatch): + mpl_test_backend = SimpleNamespace(**vars(backend_template)) + del mpl_test_backend.new_figure_manager + monkeypatch.setitem(sys.modules, "mpl_test_backend", mpl_test_backend) + mpl.use("module://mpl_test_backend") + assert type(plt.figure().canvas) == backend_template.FigureCanvasTemplate