From 49c203f98a282e2e077660fbaba1039cad858521 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Thu, 24 Oct 2024 09:34:01 -0600 Subject: [PATCH 1/4] FIX: Add update capability to interval/singleshot timer properties --- .../tests/test_backends_interactive.py | 26 ++++++++++++++++--- src/_macosx.m | 18 +++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 05f59ce39fa4..5569173ed347 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -626,11 +626,13 @@ def _impl_test_interactive_timers(): # We only want singleshot if we specify that ourselves, otherwise we want # a repeating timer import os + import sys from unittest.mock import Mock import matplotlib.pyplot as plt # increase pause duration on CI to let things spin up # particularly relevant for gtk3cairo pause_time = 2 if os.getenv("CI") else 0.5 + expected_100ms_calls = int(pause_time / 0.1) fig = plt.figure() plt.pause(pause_time) timer = fig.canvas.new_timer(0.1) @@ -638,17 +640,33 @@ def _impl_test_interactive_timers(): timer.add_callback(mock) timer.start() plt.pause(pause_time) - timer.stop() - assert mock.call_count > 1 + # NOTE: The timer is as fast as possible, but this is different between backends + # so we can't assert on the exact number but it should be faster than 100ms + assert mock.call_count > expected_100ms_calls, \ + f"Expected more than {expected_100ms_calls} calls, got {mock.call_count}" + + # Test updating the interval updates a running timer + timer.interval = 100 + mock.call_count = 0 + plt.pause(pause_time) + # GTK4 on macos runners produces about 3x as many calls as expected + # It works locally and on Linux though, so only skip when running on CI + if not (os.getenv("CI") + and "gtk4" in os.getenv("MPLBACKEND") + and sys.platform == "darwin"): + # Could be off due to when the timers actually get fired (especially on CI) + assert 1 < mock.call_count <= expected_100ms_calls + 1, \ + f"Expected less than {expected_100ms_calls + 1} calls, " \ + "got {mock.call_count}" # Now turn it into a single shot timer and verify only one gets triggered mock.call_count = 0 timer.single_shot = True - timer.start() plt.pause(pause_time) assert mock.call_count == 1 - # Make sure we can start the timer a second time + # Make sure we can start the timer after stopping + timer.stop() timer.start() plt.pause(pause_time) assert mock.call_count == 2 diff --git a/src/_macosx.m b/src/_macosx.m index 09838eccaf98..4495bb585b03 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -1789,6 +1789,18 @@ - (void)flagsChanged:(NSEvent *)event Py_RETURN_NONE; } +static PyObject* +Timer__timer_update(Timer* self) +{ + // stop and invalidate a timer if it is already running and then create a new one + // where the start() method retrieves the updated interval internally + if (self->timer) { + Timer__timer_stop_impl(self); + gil_call_method((PyObject*)self, "_timer_start"); + } + Py_RETURN_NONE; +} + static void Timer_dealloc(Timer* self) { @@ -1815,6 +1827,12 @@ - (void)flagsChanged:(NSEvent *)event {"_timer_stop", (PyCFunction)Timer__timer_stop, METH_NOARGS}, + {"_timer_set_interval", + (PyCFunction)Timer__timer_update, + METH_NOARGS}, + {"_timer_set_single_shot", + (PyCFunction)Timer__timer_update, + METH_NOARGS}, {} // sentinel }, }; From 64a677e341bfed95b48ff754d495f0306d6e887c Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Fri, 25 Oct 2024 07:06:58 -0600 Subject: [PATCH 2/4] MNT/TST: Refactor test-interactive-timers to reduce test time --- .../tests/test_backends_interactive.py | 79 +++++++++---------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 5569173ed347..8a127d619f59 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -621,55 +621,52 @@ def test_blitting_events(env): def _impl_test_interactive_timers(): + # NOTE: We run the timer tests in parallel to avoid longer sequential + # delays which adds to the testing time. Add new tests to one of + # the current event loop iterations if possible. + from unittest.mock import Mock + import matplotlib.pyplot as plt + + fig = plt.figure() + event_loop_time = 0.5 # in seconds + # A timer with <1 millisecond gets converted to int and therefore 0 # milliseconds, which the mac framework interprets as singleshot. # We only want singleshot if we specify that ourselves, otherwise we want # a repeating timer - import os - import sys - from unittest.mock import Mock - import matplotlib.pyplot as plt - # increase pause duration on CI to let things spin up - # particularly relevant for gtk3cairo - pause_time = 2 if os.getenv("CI") else 0.5 - expected_100ms_calls = int(pause_time / 0.1) - fig = plt.figure() - plt.pause(pause_time) - timer = fig.canvas.new_timer(0.1) - mock = Mock() - timer.add_callback(mock) - timer.start() - plt.pause(pause_time) + timer_repeating = fig.canvas.new_timer(0.1) + mock_repeating = Mock() + timer_repeating.add_callback(mock_repeating) + timer_repeating.start() + + timer_single_shot = fig.canvas.new_timer(100) + mock_single_shot = Mock() + timer_single_shot.add_callback(mock_single_shot) + # Start as a repeating timer then change to singleshot via the attribute + timer_single_shot.start() + timer_single_shot.single_shot = True + + fig.canvas.start_event_loop(event_loop_time) # NOTE: The timer is as fast as possible, but this is different between backends # so we can't assert on the exact number but it should be faster than 100ms - assert mock.call_count > expected_100ms_calls, \ - f"Expected more than {expected_100ms_calls} calls, got {mock.call_count}" + expected_100ms_calls = int(event_loop_time / 0.1) + assert mock_repeating.call_count > expected_100ms_calls, \ + f"Expected more than {expected_100ms_calls} calls, " \ + f"got {mock_repeating.call_count}" + assert mock_single_shot.call_count == 1 # Test updating the interval updates a running timer - timer.interval = 100 - mock.call_count = 0 - plt.pause(pause_time) - # GTK4 on macos runners produces about 3x as many calls as expected - # It works locally and on Linux though, so only skip when running on CI - if not (os.getenv("CI") - and "gtk4" in os.getenv("MPLBACKEND") - and sys.platform == "darwin"): - # Could be off due to when the timers actually get fired (especially on CI) - assert 1 < mock.call_count <= expected_100ms_calls + 1, \ - f"Expected less than {expected_100ms_calls + 1} calls, " \ - "got {mock.call_count}" - - # Now turn it into a single shot timer and verify only one gets triggered - mock.call_count = 0 - timer.single_shot = True - plt.pause(pause_time) - assert mock.call_count == 1 - - # Make sure we can start the timer after stopping - timer.stop() - timer.start() - plt.pause(pause_time) - assert mock.call_count == 2 + timer_repeating.interval = 100 + mock_repeating.call_count = 0 + # Make sure we can start the timer after stopping a singleshot timer + timer_single_shot.stop() + timer_single_shot.start() + + fig.canvas.start_event_loop(event_loop_time) + assert 1 < mock_repeating.call_count <= expected_100ms_calls + 1, \ + f"Expected less than {expected_100ms_calls + 1} calls, " \ + "got {mock.call_count}" + assert mock_single_shot.call_count == 2 plt.close("all") From 1a207a9fef1fdef8660847e9c22e954c4a0bcc56 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Fri, 25 Oct 2024 07:12:11 -0600 Subject: [PATCH 3/4] FIX: Event loop timers should only run for the specified time The implementation of start_event_loop would previously just count the number of sleeps that occurred. But this could lead to longer event loop times if flush_events() added time into the loop. We want the condition to be dependent on the end-time so we don't run our loop longer than necessary. --- lib/matplotlib/backend_bases.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 95ed49612b35..f6706d1ff16b 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2320,13 +2320,11 @@ def start_event_loop(self, timeout=0): """ if timeout <= 0: timeout = np.inf - timestep = 0.01 - counter = 0 + t_end = time.perf_counter() + timeout self._looping = True - while self._looping and counter * timestep < timeout: + while self._looping and time.perf_counter() < t_end: self.flush_events() - time.sleep(timestep) - counter += 1 + time.sleep(0.01) # Pause for 10ms def stop_event_loop(self): """ From 2d7a86f5e2dbfb7d32222a792753a155b34d8b16 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Fri, 25 Oct 2024 12:05:05 -0600 Subject: [PATCH 4/4] FIX: Add single shot update capability to TimerWx --- lib/matplotlib/backends/backend_wx.py | 4 ++++ lib/matplotlib/tests/test_backends_interactive.py | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index c7e26b92134a..438fcb3b62b9 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -69,6 +69,10 @@ def _timer_set_interval(self): if self._timer.IsRunning(): self._timer_start() # Restart with new interval. + def _timer_set_single_shot(self): + if self._timer.IsRunning(): + self._timer_start() # Restart with new interval. + @_api.deprecated( "2.0", name="wx", obj_type="backend", removal="the future", diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 8a127d619f59..c4f41e800de0 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -653,7 +653,8 @@ def _impl_test_interactive_timers(): assert mock_repeating.call_count > expected_100ms_calls, \ f"Expected more than {expected_100ms_calls} calls, " \ f"got {mock_repeating.call_count}" - assert mock_single_shot.call_count == 1 + assert mock_single_shot.call_count == 1, \ + f"Expected 1 call, got {mock_single_shot.call_count}" # Test updating the interval updates a running timer timer_repeating.interval = 100 @@ -666,7 +667,8 @@ def _impl_test_interactive_timers(): assert 1 < mock_repeating.call_count <= expected_100ms_calls + 1, \ f"Expected less than {expected_100ms_calls + 1} calls, " \ "got {mock.call_count}" - assert mock_single_shot.call_count == 2 + assert mock_single_shot.call_count == 2, \ + f"Expected 2 calls, got {mock_single_shot.call_count}" plt.close("all") pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy