diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 7c1ff265aea3..746ac2d59e7e 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -398,6 +398,8 @@ class FigureManagerTk(FigureManagerBase): The tk.Window """ + _owns_mainloop = False + def __init__(self, canvas, num, window): FigureManagerBase.__init__(self, canvas, num) self.window = window @@ -442,9 +444,8 @@ def show(self): with _restore_foreground_window_at_end(): if not self._shown: def destroy(*args): - self.window = None Gcf.destroy(self) - self.canvas._tkcanvas.bind("", destroy) + self.window.protocol("WM_DELETE_WINDOW", destroy) self.window.deiconify() else: self.canvas.draw_idle() @@ -454,15 +455,13 @@ def destroy(*args): self._shown = True def destroy(self, *args): - if self.window is not None: - #self.toolbar.destroy() - if self.canvas._idle_callback: - self.canvas._tkcanvas.after_cancel(self.canvas._idle_callback) - self.window.destroy() - if Gcf.get_num_fig_managers() == 0: - if self.window is not None: - self.window.quit() - self.window = None + if self.canvas._idle_callback: + self.canvas._tkcanvas.after_cancel(self.canvas._idle_callback) + + self.window.destroy() + + if self._owns_mainloop and not Gcf.get_num_fig_managers(): + self.window.quit() def get_window_title(self): return self.window.wm_title() @@ -883,4 +882,12 @@ def trigger_manager_draw(manager): def mainloop(): managers = Gcf.get_all_fig_managers() if managers: - managers[0].window.mainloop() + first_manager = managers[0] + manager_class = type(first_manager) + if manager_class._owns_mainloop: + return + manager_class._owns_mainloop = True + try: + first_manager.window.mainloop() + finally: + manager_class._owns_mainloop = False diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 53b21f45acdb..e79e32d0dfc6 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -64,9 +64,10 @@ def _get_running_interactive_framework(): return "wx" tkinter = sys.modules.get("tkinter") if tkinter: + codes = {tkinter.mainloop.__code__, tkinter.Misc.mainloop.__code__} for frame in sys._current_frames().values(): while frame: - if frame.f_code == tkinter.mainloop.__code__: + if frame.f_code in codes: return "tk" frame = frame.f_back if 'matplotlib.backends._macosx' in sys.modules: diff --git a/lib/matplotlib/tests/test_backend_tk.py b/lib/matplotlib/tests/test_backend_tk.py index 18ffcb40a0b1..b56b25a717fb 100644 --- a/lib/matplotlib/tests/test_backend_tk.py +++ b/lib/matplotlib/tests/test_backend_tk.py @@ -1,5 +1,11 @@ -import pytest +import os +import subprocess +import sys +import tkinter + import numpy as np +import pytest + from matplotlib import pyplot as plt @@ -26,3 +32,71 @@ def evil_blit(photoimage, aggimage, offsets, bboxptr): np.ones((4, 4, 4)), (0, 1, 2, 3), bad_boxes) + + +@pytest.mark.backend('TkAgg', skip_on_importerror=True) +def test_figuremanager_preserves_host_mainloop(): + success = False + + def do_plot(): + plt.figure() + plt.plot([1, 2], [3, 5]) + plt.close() + root.after(0, legitimate_quit) + + def legitimate_quit(): + root.quit() + nonlocal success + success = True + + root = tkinter.Tk() + root.after(0, do_plot) + root.mainloop() + + assert success + + +@pytest.mark.backend('TkAgg', skip_on_importerror=True) +@pytest.mark.flaky(reruns=3) +def test_figuremanager_cleans_own_mainloop(): + script = ''' +import tkinter +import time +import matplotlib.pyplot as plt +import threading +from matplotlib.cbook import _get_running_interactive_framework + +root = tkinter.Tk() +plt.plot([1, 2, 3], [1, 2, 5]) + +def target(): + while not 'tk' == _get_running_interactive_framework(): + time.sleep(.01) + plt.close() + if show_finished_event.wait(): + print('success') + +show_finished_event = threading.Event() +thread = threading.Thread(target=target, daemon=True) +thread.start() +plt.show(block=True) # testing if this function hangs +show_finished_event.set() +thread.join() + +''' + try: + proc = subprocess.run( + [sys.executable, "-c", script], + env={**os.environ, + "MPLBACKEND": "TkAgg", + "SOURCE_DATE_EPOCH": "0"}, + timeout=10, + stdout=subprocess.PIPE, + universal_newlines=True, + check=True + ) + except subprocess.TimeoutExpired: + pytest.fail("Most likely plot.show(block=True) hung") + except subprocess.CalledProcessError: + pytest.fail("Subprocess failed to test intended behavior") + assert proc.stdout.count("success") == 1