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): """ 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 05f59ce39fa4..c4f41e800de0 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -621,37 +621,54 @@ 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 - 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 - 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.stop() - assert mock.call_count > 1 - - # 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 - timer.start() - plt.pause(pause_time) - assert mock.call_count == 2 + 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 + 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, \ + f"Expected 1 call, got {mock_single_shot.call_count}" + + # Test updating the interval updates a running timer + 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, \ + f"Expected 2 calls, got {mock_single_shot.call_count}" plt.close("all") 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 }, };