diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ddc2b8fc731700efdefd1d7410490b19ad672f68..6065e4aca6688e9ba0cb0ca5acba552bcc4ced6d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,19 @@ This document contains the ``fslpy`` release history in reverse chronological order. + +3.6.0 (Under development) +------------------------- + + +Changed +^^^^^^^ + + +* The :class:`.TaskThread` now allows an error handler function to be + specified, which is run on the :mod:`.idle` loop. + + 3.5.3 (Tuesday 9th February 2021) --------------------------------- diff --git a/fsl/utils/idle.py b/fsl/utils/idle.py index bef726633b40f8977a3a77a429d77917f7c398b0..f8ff25349c3b3bc01465dfb9a1ebba584abf543d 100644 --- a/fsl/utils/idle.py +++ b/fsl/utils/idle.py @@ -759,10 +759,11 @@ class Task(object): """Container object which encapsulates a task that is run by a :class:`TaskThread`. """ - def __init__(self, name, func, onFinish, args, kwargs): + def __init__(self, name, func, onFinish, onError, args, kwargs): self.name = name self.func = func self.onFinish = onFinish + self.onError = onError self.args = args self.kwargs = kwargs self.enabled = True @@ -808,9 +809,16 @@ class TaskThread(threading.Thread): :arg onFinish: An optional function to be called (via :func:`idle`) when the task funtion has finished. Must be provided as - a keyword argument. If the ``func`` raises a - :class`TaskThreadVeto` error, this function will not - be called. + a keyword argument, and must itself accept no arguments. + If the ``func`` raises a :class`TaskThreadVeto` error, + this function will not be called. + + :arg onError: An optional function to be called (via :func:`idle`) + if the task funtion raises an ``Exception``. Must be + provided as a keyword argument, and must itself accept + the raised ``Exception`` object as a single argument. + If the ``func`` raises a :class`TaskThreadVeto` error, + this function will not be called. All other arguments are passed through to the task function when it is executed. @@ -821,16 +829,18 @@ class TaskThread(threading.Thread): results. .. warning:: Make sure that your task function is not expecting keyword - arguments called ``taskName`` or ``onFinish``! + arguments called ``taskName``, ``onFinish``, or + ``onError``! """ name = kwargs.pop('taskName', None) onFinish = kwargs.pop('onFinish', None) + onError = kwargs.pop('onError', None) log.debug('Enqueueing task: {} [{}]'.format( name, getattr(func, '__name__', '<unknown>'))) - t = Task(name, func, onFinish, args, kwargs) + t = Task(name, func, onFinish, onError, args, kwargs) self.__enqueued[name] = t self.__q.put(t) @@ -951,6 +961,9 @@ class TaskThread(threading.Thread): type(e).__name__, str(e)), exc_info=True) + if task.onError is not None: + idle(task.onError, e) + finally: self.__q.task_done() diff --git a/tests/test_idle.py b/tests/test_idle.py index fdee73ac7ab3ab93a02110587b6f397524df4767..e6615319eaaefcf28b7309a56570f70f8873f991 100644 --- a/tests/test_idle.py +++ b/tests/test_idle.py @@ -600,6 +600,37 @@ def test_TaskThread_onFinish(): assert onFinishCalled[0] +def test_TaskThread_onError(): + + taskCalled = [False] + onFinishCalled = [False] + onErrorCalled = [False] + + def task(): + taskCalled[0] = True + raise Exception('Task error') + + def onFinish(): + onFinishCalled[0] = True + + def onError(e): + onErrorCalled[0] = str(e) + + tt = idle.TaskThread() + tt.start() + + tt.enqueue(task, onFinish=onFinish, onError=onError) + + time.sleep(0.5) + + tt.stop() + tt.join() + + assert taskCalled[0] + assert onErrorCalled[0] == 'Task error' + assert not onFinishCalled[0] + + def test_TaskThread_isQueued(): called = [False]