8000 Move show() to somewhere naturally inheritable. · matplotlib/matplotlib@27a4e41 · GitHub
[go: up one dir, main page]

Skip to content

Commit 27a4e41

Browse files
committed
Move show() to somewhere naturally inheritable.
It's actually not clear whether to move it to FigureCanvas or FigureManager. FigureCanvas already has start_event_loop (cf. start_main_loop), but pyplot_show/start_main_loop is more a global/pyplot-only concept, so perhaps it belongs to FigureManager instead? OTOH, being on canvas_class makes it easier for switch_backend to access it (it doesn't need to go through `canvas_class.manager_class.pyplot_show`, which is relevant considering that some designs of inheritable backends didn't even have a manager_class attribute).
1 parent 4455dc9 commit 27a4e41

File tree

11 files changed

+156
-80
lines changed

11 files changed

+156
-80
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2776,6 +2776,46 @@ def create_with_canvas(cls, canvas_class, figure, num):
27762776
"""
27772777
return cls(canvas_class(figure), num)
27782778

2779+
@classmethod
2780+
def start_main_loop(cls):
2781+
"""
2782+
Start the main event loop.
2783+
2784+
Interactive backends need to reimplement this method or
2785+
`~.FigureManagerBase.pyplot_show`.
2786+
"""
2787+
2788+
@classmethod
2789+
def pyplot_show(cls, *, block=None):
2790+
"""
2791+
Show all figures. This method is the implementation of `.pyplot.show`.
2792+
2793+
Interactive backends need to reimplement this method or
2794+
`~.FigureManagerBase.start_main_loop`.
2795+
2796+
Parameters
2797+
----------
2798+
block : bool, optional
2799+
Whether to block by calling ``start_main_loop``. The default,
2800+
None, means to block if we are neither in IPython's ``%pylab`` mode
2801+
nor in ``interactive`` mode.
2802+
"""
2803+
managers = Gcf.get_all_fig_managers()
2804+
if not managers:
2805+
return
2806+
for manager in managers:
2807+
try:
2808+
manager.show() # Emits a warning for non-interactive backend.
2809+
except NonGuiException as exc:
2810+
_api.warn_external(str(exc))
2811+
if block is None:
2812+
# Hack: Are we in IPython's pylab mode?
2813+
from matplotlib import pyplot
2814+
ipython_pylab = hasattr(pyplot.show, "_needmain")
2815+
block = not ipython_pylab and not is_interactive()
2816+
if block:
2817+
cls.start_main_loop()
2818+
27792819
def show(self):
27802820
"""
27812821
For GUI backends, show the figure window and redraw.
@@ -3460,7 +3500,13 @@ def new_figure_manager_given_figure(cls, num, figure):
34603500

34613501
@classmethod
34623502
def draw_if_interactive(cls):
3463-
if cls.mainloop is not None and is_interactive():
3503+
canvas_class = cls.FigureCanvas
3504+
manager_class = canvas_class.manager_class
3505+
interactive_backend = (
3506+
manager_class.start_main_loop != FigureManagerBase.start_main_loop
3507+
or canvas_class.show != FigureCanvasBase.show
3508+
)
3509+
if interactive_backend and is_interactive():
34643510
manager = Gcf.get_active()
34653511
if manager:
34663512
manager.canvas.draw_idle()

lib/matplotlib/backends/_backend_gtk.py

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from matplotlib import _api, backend_tools, cbook
1010
from matplotlib._pylab_helpers import Gcf
1111
from matplotlib.backend_bases import (
12-
_Backend, FigureManagerBase, NavigationToolbar2, TimerBase F438 )
12+
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
13+
TimerBase)
1314
from matplotlib.backend_tools import Cursors
1415

1516
import gi
@@ -118,6 +119,10 @@ def _on_timer(self):
118119
return False
119120

120121

122+
class _FigureCanvasGTK(FigureCanvasBase):
123+
_timer_cls = TimerGTK
124+
125+
121126
class _FigureManagerGTK(FigureManagerBase):
122127
"""
123128
Attributes
@@ -197,6 +202,25 @@ def destroy(self, *args):
197202
self.window.destroy()
198203
self.canvas.destroy()
199204

205+
@classmethod
206+
def start_main_loop(cls):
207+
global _application
208+
if _application is None:
209+
return
210+
211+
try:
212+
_application.run() # Quits when all added windows close.
213+
except KeyboardInterrupt:
214+
# Ensure all windows can process their close event from
215+
# _shutdown_application.
216+
context = GLib.MainContext.default()
217+
while context.pending():
218+
context.iteration(True)
219+
raise
220+
finally:
221+
# Running after quit is undefined, so create a new one next time.
222+
_application = None
223+
200224
def show(self):
201225
# show the figure window
202226
self.window.show()
@@ -305,21 +329,4 @@ def trigger(self, *args):
305329

306330

307331
class _BackendGTK(_Backend):
308-
@staticmethod
309-
def mainloop():
310-
global _application
311-
if _application is None:
312-
return
313-
314-
try:
315-
_application.run() # Quits when all added windows close.
316-
except KeyboardInterrupt:
317-
# Ensure all windows can process their close event from
318-
# _shutdown_application.
319-
context = GLib.MainContext.default()
320-
while context.pending():
321-
context.iteration(True)
322-
raise
323-
finally:
324-
# Running after quit is undefined, so create a new one next time.
325-
_application = None
332+
mainloop = _FigureManagerGTK.start_main_loop

lib/matplotlib/backends/_backend_tk.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,20 @@ def create_with_canvas(cls, canvas_class, figure, num):
471471
canvas.draw_idle()
472472
return manager
473473

474+
@classmethod
475+
def start_main_loop(cls):
476+
managers = Gcf.get_all_fig_managers()
477+
if managers:
478+
first_manager = managers[0]
479+
manager_class = type(first_manager)
480+
if manager_class._owns_mainloop:
481+
return
482+
manager_class._owns_mainloop = True
483+
try:
484+
first_manager.window.mainloop()
485+
finally:
486+
manager_class._owns_mainloop = False
487+
474488
def _update_window_dpi(self, *args):
475489
newdpi = self._window_dpi.get()
476490
self.window.call('tk', 'scaling', newdpi / 72)
@@ -1002,18 +1016,6 @@ def trigger(self, *args):
10021016

10031017
@_Backend.export
10041018
class _BackendTk(_Backend):
1019+
FigureCanvas = FigureCanvasTk
10051020
FigureManager = FigureManagerTk
1006-
1007-
@staticmethod
1008-
def mainloop():
1009-
managers = Gcf.get_all_fig_managers()
1010-
if managers:
1011-
first_manager = managers[0]
1012-
manager_class = type(first_manager)
1013-
if manager_class._owns_mainloop:
1014-
return
1015-
manager_class._owns_mainloop = True
1016-
try:
1017-
first_manager.window.mainloop()
1018-
finally:
1019-
manager_class._owns_mainloop = False
1021+
mainloop = FigureManagerTk.start_main_loop

lib/matplotlib/backends/backend_gtk3.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from gi.repository import Gio, GLib, GObject, Gtk, Gdk
2727
from . import _backend_gtk
2828
from ._backend_gtk import (
29-
_BackendGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
29+
_BackendGTK, _FigureCanvasGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
3030
TimerGTK as TimerGTK3,
3131
)
3232
from ._backend_gtk import backend_version # noqa: F401 # pylint: disable=W0611
@@ -68,9 +68,8 @@ def _mpl_to_gtk_cursor(mpl_cursor):
6868
_backend_gtk.mpl_to_gtk_cursor_name(mpl_cursor))
6969

7070

71-
class FigureCanvasGTK3(FigureCanvasBase, Gtk.DrawingArea):
71+
class FigureCanvasGTK3(_FigureCanvasGTK, Gtk.DrawingArea):
7272
required_interactive_framework = "gtk3"
73-
_timer_cls = TimerGTK3
7473
manager_class = _api.classproperty(lambda cls: FigureManagerGTK3)
7574
# Setting this as a static constant prevents
7675
# this resulting expression from leaking

lib/matplotlib/backends/backend_gtk4.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,15 @@
2323
from gi.repository import Gio, GLib, GObject, Gtk, Gdk, GdkPixbuf
2424
from . import _backend_gtk
2525
from ._backend_gtk import (
26-
_BackendGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
26+
_BackendGTK, _FigureCanvasGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
2727
TimerGTK as TimerGTK4,
2828
)
2929
from ._backend_gtk import backend_version # noqa: F401 # pylint: disable=W0611
3030

3131

32-
class FigureCanvasGTK4(FigureCanvasBase, Gtk.DrawingArea):
32+
class FigureCanvasGTK4(_FigureCanvasGTK, Gtk.DrawingArea):
3333
required_interactive_framework = "gtk4"
3434
supports_blit = False
35-
_timer_cls = TimerGTK4
3635
manager_class = _api.classproperty(lambda cls: FigureManagerGTK4)
3736
_context_is_scaled = False
3837

lib/matplotlib/backends/backend_macosx.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ def close(self):
162162
Gcf.destroy(self)
163163
self.canvas.flush_events()
164164

165+
@classmethod
166+
def start_main_loop(cls):
167+
_macosx.show()
168+
165169
def show(self):
166170
if not self._shown:
167171
self._show()
@@ -174,7 +178,4 @@ def show(self):
174178
class _BackendMac(_Backend):
175179
FigureCanvas = FigureCanvasMac
176180
FigureManager = FigureManagerMac
177-
178-
@staticmethod
179-
def mainloop():
180-
_macosx.show()
181+
mainloop = FigureManagerMac.start_main_loop

lib/matplotlib/backends/backend_qt.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,12 @@ def resize(self, width, height):
569569
self.canvas.resize(width, height)
570570
self.window.resize(width + extra_width, height + extra_height)
571571

572+
@classmethod
573+
def start_main_loop(cls):
574+
qapp = QtWidgets.QApplication.instance()
575+
with _maybe_allow_interrupt(qapp):
576+
qt_compat._exec(qapp)
577+
572578
def show(self):
573579
self.window.show()
574580
if mpl.rcParams['figure.raise_window']:
@@ -999,9 +1005,4 @@ def trigger(self, *args, **kwargs):
9991005
class _BackendQT(_Backend):
10001006
FigureCanvas = FigureCanvasQT
10011007
FigureManager = FigureManagerQT
1002-
1003-
@staticmethod
1004-
def mainloop():
1005-
qapp = QtWidgets.QApplication.instance()
1006-
with _maybe_allow_interrupt(qapp):
1007-
qt_compat._exec(qapp)
1008+
mainloop = FigureManagerQT.start_main_loop

lib/matplotlib/backends/backend_webagg.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,24 @@ def run(self):
5151
class FigureManagerWebAgg(core.FigureManagerWebAgg):
5252
_toolbar2_class = core.NavigationToolbar2WebAgg
5353

54+
@classmethod
55+
def pyplot_show(cls, *, block=None):
56+
WebAggApplication.initialize()
57+
58+
url = "http://{address}:{port}{prefix}".format(
59+
address=WebAggApplication.address,
60+
port=WebAggApplication.port,
61+
prefix=WebAggApplication.url_prefix)
62+
63+
if mpl.rcParams['webagg.open_in_browser']:
64+
import webbrowser
65+
if not webbrowser.open(url):
66+
print("To view figure, visit {0}".format(url))
67+
else:
68+
print("To view figure, visit {0}".format(url))
69+
70+
WebAggApplication.start()
71+
5472

5573
class FigureCanvasWebAgg(core.FigureCanvasWebAggCore):
5674
manager_class = FigureManagerWebAgg
@@ -305,21 +323,3 @@ def ipython_inline_display(figure):
305323
class _BackendWebAgg(_Backend):
306324
FigureCanvas = FigureCanvasWebAgg
307325
FigureManager = FigureManagerWebAgg
308-
309-
@staticmethod
310-
def show(*, block=None):
311-
WebAggApplication.initialize()
312-
313-
url = "http://{address}:{port}{prefix}".format(
314-
address=WebAggApplication.address,
315-
port=WebAggApplication.port,
316-
prefix=WebAggApplication.url_prefix)
317-
318-
if mpl.rcParams['webagg.open_in_browser']:
319-
import webbrowser
320-
if not webbrowser.open(url):
321-
print("To view figure, visit {0}".format(url))
322-
else:
323-
print("To view figure, visit {0}".format(url))
324-
325-
WebAggApplication.start()

lib/matplotlib/backends/backend_wx.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,13 @@ def create_with_canvas(cls, canvas_class, figure, num):
991991
figure.canvas.draw_idle()
992992
return manager
993993

994+
@classmethod
995+
def start_main_loop(cls):
996+
if not wx.App.IsMainLoopRunning():
997+
wxapp = wx.GetApp()
998+
if wxapp is not None:
999+
wxapp.MainLoop()
1000+
9941001
def show(self):
9951002
# docstring inherited
9961003
self.frame.Show()
@@ -1364,10 +1371,4 @@ def trigger(self, *args, **kwargs):
13641371
class _BackendWx(_Backend):
13651372
FigureCanvas = FigureCanvasWx
13661373
FigureManager = FigureManagerWx
1367-
1368-
@staticmethod
1369-
def mainloop():
1370-
if not wx.App.IsMainLoopRunning():
1371-
wxapp = wx.GetApp()
1372-
if wxapp is not None:
1373-
wxapp.MainLoop()
1374+
mainloop = FigureManagerWx.start_main_loop

lib/matplotlib/pyplot.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@
5555
from matplotlib import _pylab_helpers, interactive
5656
from matplotlib import cbook
5757
from matplotlib import _docstring
58-
from matplotlib.backend_bases import FigureCanvasBase, MouseButton
58+
from matplotlib.backend_bases import (
59+
FigureCanvasBase, FigureManagerBase, MouseButton)
5960
from matplotlib.figure import Figure, FigureBase, figaspect
6061
from matplotlib.gridspec import GridSpec, SubplotSpec
6162
from matplotlib import rcParams, rcParamsDefault, get_backend, rcParamsOrig
@@ -279,17 +280,18 @@ def switch_backend(newbackend):
279280
# Classically, backends can directly export these functions. This should
280281
# keep working for backcompat.
281282
new_figure_manager = getattr(backend_mod, "new_figure_manager", None)
282-
# show = getattr(backend_mod, "show", None)
283+
show = getattr(backend_mod, "show", None)
284+
283285
# In that classical approach, backends are implemented as modules, but
284286
# "inherit" default method implementations from backend_bases._Backend.
285287
# This is achieved by creating a "class" that inherits from
286288
# backend_bases._Backend and whose body is filled with the module globals.
287289
class backend_mod(matplotlib.backend_bases._Backend):
288290
locals().update(vars(backend_mod))
289291

290-
# However, the newer approach for defining new_figure_manager (and, in
291-
# the future, show) is to derive them from canvas methods. In that case,
292-
# also update backend_mod accordingly; also, per-backend customization of
292+
# However, the newer approach for defining new_figure_manager and
293+
# show is to derive them from canvas methods. In that case, also
294+
# update backend_mod accordingly; also, per-backend customization of
293295
# draw_if_interactive is disabled.
294296
if new_figure_manager is None:
295297
def new_figure_manager_given_figure(num, figure):
@@ -310,6 +312,12 @@ def draw_if_interactive():
310312
backend_mod.new_figure_manager = new_figure_manager
311313
backend_mod.draw_if_interactive = draw_if_interactive
312314

315+
# If the manager explicitly overrides pyplot_show, use it even if a global
316+
# show is already present, as the latter may be here for backcompat.
317+
if (canvas_class.manager_class.pyplot_show != FigureManagerBase.pyplot_show
318+
or show is None):
319+
backend_mod.show = canvas_class.manager_class.pyplot_show
320+
313321
_log.debug("Loaded backend %s version %s.",
314322
newbackend, backend_mod.backend_version)
315323

lib/matplotlib/tests/test_backend_template.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import sys
66
from types import SimpleNamespace
7+
from unittest.mock import MagicMock
78

89
import matplotlib as mpl
910
from matplotlib import pyplot as plt
@@ -27,3 +28,14 @@ def test_load_old_api(monkeypatch):
2728
mpl.use("module://mpl_test_backend")
2829
assert type(plt.figure().canvas) == FigureCanvasTemplate
2930
plt.draw_if_interactive()
31+
32+
33+
def test_show(monkeypatch):
34+
mpl_test_backend = SimpleNamespace(**vars(backend_template))
35+
mock_show = backend_template.FigureManagerTemplate.pyplot_show = \
36+
MagicMock()
37+
del mpl_test_backend.show
38+
monkeypatch.setitem(sys.modules, "mpl_test_backend", mpl_test_backend)
39+
mpl.use("module://mpl_test_backend")
40+
plt.show()
41+
mock_show.assert_called_with()

0 commit comments

Comments
 (0)
0