diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 731efdab4e92635d3a9da0cbc4d9799a50df205c..4d7b6c893a2deaf0b083dfd334230147b27a3cbc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,8 @@ Fixed * Adjusted the :func:`.featdesign.loadFEATDesignFile` function to handle missing values (!469). +* Fix to the :class:`.Notifier` class involving handling of callback functions + that have been garbage-collected (!470). 3.22.0 (Monday 17th February 2025) diff --git a/fsl/tests/test_notifier.py b/fsl/tests/test_notifier.py index c85df509f9940bbbe4a0b64f0d6a4614bf10802e..d3ead8c66cf808b70139782ed6c4872023a83f84 100644 --- a/fsl/tests/test_notifier.py +++ b/fsl/tests/test_notifier.py @@ -204,3 +204,31 @@ def test_skip(): t.notify(topic='topic') assert default_called[0] == 14 assert topic_called[ 0] == 6 + + +# Make sure there is no error +# if a callback function is GC'd +# fsl/fslpy!470 +def test_gc(): + + class Thing(notifier.Notifier): + pass + + t = Thing() + + called = [] + + def callback(thing, topic, value): + called.append((thing, topic, value)) + + t.register('callback', callback) + + t.notify() + assert called == [(t, None, None)] + + called[:] = [] + callback = None + del callback + + t.notify() + assert called == [] diff --git a/fsl/utils/notifier.py b/fsl/utils/notifier.py index bb039e636ff30fd1ee29256fcf210f3d651b5d4e..6a46df394c2767f808c49909034fc467eaa454d4 100644 --- a/fsl/utils/notifier.py +++ b/fsl/utils/notifier.py @@ -66,7 +66,12 @@ class _Listener: positional arguments - see :meth:`Notifier.register` for details. """ - func = self.callback + func = self.callback + + # the function may have been GC'd + if func is None: + return False + spec = inspect.signature(func) posargs = 0 varargs = False @@ -377,9 +382,6 @@ class Notifier: callback = listener.callback name = listener.name - if listener.expectsArguments: args = (self, topic, value) - else: args = () - # The callback, or the owner of the # callback function may have been # gc'd - remove it if this is the case. @@ -387,12 +389,16 @@ class Notifier: log.debug('Listener %s has been gc\'d - ' 'removing from list', name) self.__listeners[listener.topic].pop(name) + continue - elif not listener.enabled: + if not listener.enabled: continue - elif listener.runOnIdle: idle.idle(callback, *args) - else: callback( *args) + if listener.expectsArguments: args = (self, topic, value) + else: args = () + + if listener.runOnIdle: idle.idle(callback, *args) + else: callback( *args) def __getListeners(self, topic):