From 30da2c446f332fd86a586e58536dbec4818c1cdd Mon Sep 17 00:00:00 2001 From: DavidVadnais Date: Fri, 4 Sep 2026 09:26:52 -1000 Subject: [PATCH] animation.py: Stop event sources instead of nullifying them on animation stop --- .../animation_stop_event_stop_not_null.rst | 26 +++++++++++++++ lib/matplotlib/animation.py | 4 +-- lib/matplotlib/animation.pyi | 2 +- lib/matplotlib/tests/test_animation.py | 32 ++++++++++++++++++- 4 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/animation_stop_event_stop_not_null.rst diff --git a/doc/api/next_api_changes/behavior/animation_stop_event_stop_not_null.rst b/doc/api/next_api_changes/behavior/animation_stop_event_stop_not_null.rst new file mode 100644 index 000000000000..f3ea08bce501 --- /dev/null +++ b/doc/api/next_api_changes/behavior/animation_stop_event_stop_not_null.rst @@ -0,0 +1,26 @@ +``TimedAnimation``/``Animation`` now perform .stop() on mpl_disconnect +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously these classes set ``self.event_source = None`` when +``self._fig.canvas.mpl_disconnect`` was called. Now they run +``self.event_source.stop()``. + +This hange is for issue `30622 `__. + +The following code snippet would before have returned None but would now return a +``matplotlib.backend_bases.TimerBase``. Code that used None to check for successful +stoppage should be updated. + +.. code-block:: python + + import matplotlib + matplotlib.use('agg') + import matplotlib.pyplot as plt + from matplotlib.animation import FuncAnimation + + fig, ax = plt.subplots() + anim = FuncAnimation(fig, lambda f: [], frames=3, repeat=False) + anim._start() + while anim._step(): + pass + print(anim.event_source) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index ad303cbb92e9..b29885646a56 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -946,7 +946,7 @@ def _stop(self, *args): self._fig.canvas.mpl_disconnect(self._resize_id) self._fig.canvas.mpl_disconnect(self._close_id) self.event_source.remove_callback(self._step) - self.event_source = None + self.event_source.stop() def save(self, filename, writer=None, fps=None, dpi=None, codec=None, bitrate=None, extra_args=None, metadata=None, extra_anim=None, @@ -1483,7 +1483,7 @@ def _step(self, *args): # Remove the resize callback if we were blitting self._fig.canvas.mpl_disconnect(self._resize_id) self._fig.canvas.mpl_disconnect(self._close_id) - self.event_source = None + self.event_source.stop() return False self.event_source.interval = self._interval diff --git a/lib/matplotlib/animation.pyi b/lib/matplotlib/animation.pyi index e90a0103aefd..4c477be23063 100644 --- a/lib/matplotlib/animation.pyi +++ b/lib/matplotlib/animation.pyi @@ -160,7 +160,7 @@ class EventSourceProtocol(Protocol): class Animation: frame_seq: Iterable[Artist] - event_source: EventSourceProtocol | None # TODO: We should remove None + event_source: EventSourceProtocol def __init__( self, fig: Figure, event_source: EventSourceProtocol, blit: bool = ... ) -> None: ... diff --git a/lib/matplotlib/tests/test_animation.py b/lib/matplotlib/tests/test_animation.py index 9beb025c2a2e..70969f78cb35 100644 --- a/lib/matplotlib/tests/test_animation.py +++ b/lib/matplotlib/tests/test_animation.py @@ -13,7 +13,7 @@ import matplotlib as mpl from matplotlib import pyplot as plt from matplotlib import animation -from matplotlib.animation import PillowWriter +from matplotlib.animation import PillowWriter, FuncAnimation from matplotlib.testing.decorators import check_figures_equal @@ -571,3 +571,33 @@ def test_animation_with_transparency(): # Check that the alpha channel is not 255, so frame has transparency assert frame.getextrema()[3][0] < 255 plt.close(fig) + + +def test_event_source_stops_leaves_event(): + """ + Added for issue #30622 + + Tests that animations now stop the event source instead + of destroying it on animation completion. + """ + fig, ax = plt.subplots() + anim = FuncAnimation(fig, lambda f: [], frames=3, repeat=False) + anim._start() + while anim._step(): + pass # goes through all 3 steps + assert anim.event_source is not None + plt.close(fig) + + +def test_event_source_stops_early_leaves_event(): + """ + Added for issue #30622 + + Tests that animations now stop the event source instead + of destroying it on animation early stop. + """ + fig, ax = plt.subplots() + anim = FuncAnimation(fig, lambda f: [], frames=3) + anim._start() + plt.close(fig) + assert anim.event_source is not None