8000 Merge pull request #26022 from greglucas/macosx-nstimer · matplotlib/matplotlib@caa93d7 · GitHub
[go: up one dir, main page]

Skip to content

Commit caa93d7

Browse files
authored
Merge pull request #26022 from greglucas/macosx-nstimer
MNT/FIX: macosx change Timer to NSTimer instance
2 parents b0121b6 + dc38128 commit caa93d7

File tree

4 files changed

+64
-58
lines changed

4 files changed

+64
-58
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1163,7 +1163,8 @@ def interval(self):
11631163
def interval(self, interval):
11641164
# Force to int since none of the backends actually support fractional
11651165
# milliseconds, and some error or give warnings.
1166-
interval = int(interval)
1166+
# Some backends also fail when interval == 0, so ensure >= 1 msec
1167+
interval = max(int(interval), 1)
11671168
self._interval = interval
11681169
self._timer_set_interval()
11691170

lib/matplotlib/backends/backend_macosx.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def callback_func(callback, timer):
6969
self._timers.remove(timer)
7070
timer.stop()
7171
timer = self.new_timer(interval=0)
72+
timer.single_shot = True
7273
timer.add_callback(callback_func, callback, timer)
7374
self._timers.add(timer)
7475
timer.start()

lib/matplotlib/tests/test_backends_interactive.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,3 +653,48 @@ def test_figure_leak_20490(env, time_mem):
653653

654654
growth = int(result.stdout)
655655
assert growth <= acceptable_memory_leakage
656+
657+
658+
def _impl_test_interactive_timers():
659+
# A timer with <1 millisecond gets converted to int and therefore 0
660+
# milliseconds, which the mac framework interprets as singleshot.
661+
# We only want singleshot if we specify that ourselves, otherwise we want
662+
# a repeating timer
663+
import os
664+
from unittest.mock import Mock
665+
import matplotlib.pyplot as plt
666+
# increase pause duration on CI to let things spin up
667+
# particularly relevant for gtk3cairo
668+
pause_time = 2 if os.getenv("CI") else 0.5
669+
fig = plt.figure()
670+
plt.pause(pause_time)
671+
timer = fig.canvas.new_timer(0.1)
672+
mock = Mock()
673+
timer.add_callback(mock)
674+
timer.start()
675+
plt.pause(pause_time)
676+
timer.stop()
677+
assert mock.call_count > 1
678+
679+
# Now turn it into a single shot timer and verify only one gets triggered
680+
mock.call_count = 0
681+
timer.single_shot = True
682+
timer.start()
683+
plt.pause(pause_time)
684+
assert mock.call_count == 1
685+
686+
# Make sure we can start the timer a second time
687+
timer.start()
688+
plt.pause(pause_time)
689+
assert mock.call_count == 2
690+
plt.close("all")
691+
692+
693+
@pytest.mark.parametrize("env", _get_testable_interactive_backends())
694+
def test_interactive_timers(env):
695+
if env["MPLBACKEND"] == "gtk3cairo" and os.getenv("CI"):
696+
pytest.skip("gtk3cairo timers do not work in remote CI")
697+
if env["MPLBACKEND"] == "wx":
698+
pytest.skip("wx backend is deprecated; tests failed on appveyor")
699+
_run_helper(_impl_test_interactive_timers,
700+
timeout=_test_timeout, extra_env=env)

src/_macosx.m

Lines changed: 16 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1822,93 +1822,53 @@ - (void)flagsChanged:(NSEvent *)event
18221822

18231823
typedef struct {
18241824
PyObject_HEAD
1825-
CFRunLoopTimerRef timer;
1825+
NSTimer* timer;
1826+
18261827
} Timer;
18271828

18281829
static PyObject*
18291830
Timer_new(PyTypeObject* type, PyObject *args, PyObject *kwds)
18301831
{
18311832
lazy_init();
18321833
Timer* self = (Timer*)type->tp_alloc(type, 0);
1833-
if (!self) { return NULL; }
1834+
if (!self) {
1835+
return NULL;
1836+
}
18341837
self->timer = NULL;
18351838
return (PyObject*) self;
18361839
}
18371840

18381841
static PyObject*
18391842
Timer_repr(Timer* self)
18401843
{
1841-
return PyUnicode_FromFormat("Timer object %p wrapping CFRunLoopTimerRef %p",
1844+
return PyUnicode_FromFormat("Timer object %p wrapping NSTimer %p",
18421845
(void*) self, (void*)(self->timer));
18431846
}
18441847

1845-
static void timer_callback(CFRunLoopTimerRef timer, void* info)
1846-
{
1847-
gil_call_method(info, "_on_timer");
1848-
}
1849-
1850-
static void context_cleanup(const void* info)
1851-
{
1852-
Py_DECREF((PyObject*)info);
1853-
}
1854-
18551848
static PyObject*
18561849
Timer__timer_start(Timer* self, PyObject* args)
18571850
{
1858-
CFRunLoopRef runloop;
1859-
CFRunLoopTimerRef timer;
1860-
CFRunLoopTimerContext context;
1861-
CFAbsoluteTime firstFire;
1862-
CFTimeInterval interval;
1851+
NSTimeInterval interval;
18631852
PyObject* py_interval = NULL, * py_single = NULL, * py_on_timer = NULL;
18641853
int single;
1865-
runloop = CFRunLoopGetMain();
1866-
if (!runloop) {
1867-
PyErr_SetString(PyExc_RuntimeError, "Failed to obtain run loop");
1868-
return NULL;
1869-
}
18701854
if (!(py_interval = PyObject_GetAttrString((PyObject*)self, "_interval"))
18711855
|| ((interval = PyFloat_AsDouble(py_interval) / 1000.), PyErr_Occurred())
18721856
|| !(py_single = PyObject_GetAttrString((PyObject*)self, "_single"))
18731857
|| ((single = PyObject_IsTrue(py_single)) == -1)
18741858
|| !(py_on_timer = PyObject_GetAttrString((PyObject*)self, "_on_timer"))) {
18751859
goto exit;
18761860
}
1877-
// (current time + interval) is time of first fire.
1878-
firstFire = CFAbsoluteTimeGetCurrent() + interval;
1879-
if (single) {
1880-
interval = 0;
1881-
}
18821861
if (!PyMethod_Check(py_on_timer)) {
18831862
PyErr_SetString(PyExc_RuntimeError, "_on_timer should be a Python method");
18841863
goto exit;
18851864
}
1886-
Py_INCREF(self);
1887-
context.version = 0;
1888-
context.retain = NULL;
1889-
context.release = context_cleanup;
1890-
context.copyDescription = NULL;
1891-
context.inf 10000 o = self;
1892-
timer = CFRunLoopTimerCreate(kCFAllocatorDefault,
1893-
firstFire,
1894-
interval,
1895-
0,
1896-
0,
1897-
timer_callback,
1898-
&context);
1899-
if (!timer) {
1900-
PyErr_SetString(PyExc_RuntimeError, "Failed to create timer");
1901-
goto exit;
1902-
}
1903-
if (self->timer) {
1904-
CFRunLoopTimerInvalidate(self->timer);
1905-
CFRelease(self->timer);
1906-
}
1907-
CFRunLoopAddTimer(runloop, timer, kCFRunLoopCommonModes);
1908-
/* Don't release the timer here, since the run loop may be destroyed and
1909-
* the timer lost before we have a chance to decrease the reference count
1910-
* of the attribute */
1911-
self->timer = timer;
1865+
1866+
// hold a reference to the timer so we can invalidate/stop it later
1867+
self->timer = [NSTimer scheduledTimerWithTimeInterval: interval
1868+
repeats: !single
1869+
block: ^(NSTimer *timer) {
1870+
gil_call_method((PyObject*)self, "_on_timer");
1871+
}];
19121872
exit:
19131873
Py_XDECREF(py_interval);
19141874
Py_XDECREF(py_single);
@@ -1924,8 +1884,7 @@ static void context_cleanup(const void* info)
19241884
Timer__timer_stop(Timer* self)
19251885
{
19261886
if (self->timer) {
1927-
CFRunLoopTimerInvalidate(self->timer);
1928-
CFRelease(self->timer);
1887+
[self->timer invalidate];
19291888
self->timer = NULL;
19301889
}
19311890
Py_RETURN_NONE;
@@ -1946,7 +1905,7 @@ static void context_cleanup(const void* info)
19461905
.tp_repr = (reprfunc)Timer_repr,
19471906
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
19481907
.tp_new = (newfunc)Timer_new,
1949-
.tp_doc = "A Timer object wraps a CFRunLoopTimerRef and can add it to the event loop.",
1908+
.tp_doc = "A Timer object that contains an NSTimer that gets added to the event loop when started.",
19501909
.tp_methods = (PyMethodDef[]){ // All docstrings are inherited.
19511910
{"_timer_start",
19521911
(PyCFunction)Timer__timer_start,

0 commit comments

Comments
 (0)
0