diff --git a/fsl/utils/async.py b/fsl/utils/async.py index 4b403d794e101daf666acbb2731218d532784e99..a967f6e3d368ff5a76f0d74618ae589fb1ad3391 100644 --- a/fsl/utils/async.py +++ b/fsl/utils/async.py @@ -22,6 +22,7 @@ Idle tasks idle idleWhen inIdle + cancelIdle getIdleTimeout setIdleTimeout @@ -180,8 +181,8 @@ loop. """ -_idleQueueSet = set() -"""A ``set`` containing the names of all named tasks which are +_idleQueueDict = {} +"""A ``dict`` containing the names of all named tasks which are currently queued on the idle loop (see the ``name`` parameter to the :func:`idle` function). """ @@ -259,7 +260,7 @@ def _wxIdleLoop(ev): import wx global _idleQueue - global _idleQueueSet + global _idleQueueDict global _idleTimer global _idleCallRate @@ -308,7 +309,8 @@ def _wxIdleLoop(ev): taskName, type(e).__name__, str(e)), exc_info=True) if task.name is not None: - _idleQueueSet.discard(task.name) + try: _idleQueueDict.pop(task.name) + except KeyError: pass if _idleQueue.qsize() > queueSizeOffset: ev.RequestMore() @@ -320,9 +322,20 @@ def inIdle(taskName): """Returns ``True`` if a task with the given name is queued on the idle loop (or is currently running), ``False`` otherwise. """ - global _idleQueueSet - return taskName in _idleQueueSet + global _idleQueueDict + return taskName in _idleQueueDict + + +def cancelIdle(taskName): + """If a task with the given ``taskName`` is in the idle queue, it + is cancelled. If the task is already running, it cannot be cancelled. + + A ``KeyError`` is raised if no task called ``taskName`` exists. + """ + global _idleQueueDict + _idleQueueDict[taskName].timeout = -1 + def idle(task, *args, **kwargs): """Run the given task on a ``wx.EVT_IDLE`` event. @@ -344,6 +357,13 @@ def idle(task, *args, **kwargs): scheduled to be called on the idle loop, the function is not called, and is dropped from the queue. + :arg dropIfQueued: Optional. If provided, must be provided as a keyword + argument. If ``True``, and a task with the given + ``name`` is already enqueud, that function is dropped + from the queue, and the new task is enqueued. Defaults + to ``False``. This argument takes precedence over the + ``skipIfQueued`` argument. + :arg skipIfQueued: Optional. If provided, must be provided as a keyword argument. If ``True``, and a task with the given ``name`` is already enqueud, (or is running), the @@ -369,22 +389,28 @@ def idle(task, *args, **kwargs): This is because, if the required time has not elapsed when the task is popped from the queue, it will be re-queued. + .. note:: If you schedule multiple tasks with the same ``name``, and you + do not use the ``skipIfQueued`` or ``dropIfQueued`` arguments, + all of those tasks will be executed, but you will only be able + to query/cancel the most recently enqueued task. .. note:: You will run into difficulties if you schedule a function that expects/accepts its own keyword arguments called ``name``, - ``skipIfQueued``, ``after``, ``timeout``, or ``alwaysQueue``. + ``skipIfQueued``, ``dropIfQueued``, ``after``, ``timeout``, or + ``alwaysQueue``. """ global _idleRegistered global _idleTimer global _idleQueue - global _idleQueueSet + global _idleQueueDict schedtime = time.time() timeout = kwargs.pop('timeout', 0) after = kwargs.pop('after', 0) name = kwargs.pop('name', None) - skipIfQueued = kwargs.pop('skipIfQueued', None) + dropIfQueued = kwargs.pop('dropIfQueued', False) + skipIfQueued = kwargs.pop('skipIfQueued', False) alwaysQueue = kwargs.pop('alwaysQueue', False) if alwaysQueue or _haveWX(): @@ -399,10 +425,24 @@ def idle(task, *args, **kwargs): _idleTimer.Bind(wx.EVT_TIMER, _wxIdleLoop) - if name is not None and skipIfQueued and inIdle(name): - log.debug('Idle task ({}) is already queued - dropping ' - 'it'.format(getattr(task, '__name__', '<unknown>'))) - return + if name is not None and inIdle(name): + + if dropIfQueued: + + # The cancelIdle function sets the old + # task timeout to -1, so it won't get + # executed. But the task is left in the + # _idleQueue, and in the _idleQueueDict. + # In the latter, the old task gets + # overwritten with the new task below. + cancelIdle(name) + log.debug('Idle task ({}) is already queued - ' + 'dropping the old task'.format(name)) + + elif skipIfQueued: + log.debug('Idle task ({}) is already queued ' + '- skipping it'.format(name)) + return log.debug('Scheduling idle task ({}) on wx idle ' 'loop'.format(getattr(task, '__name__', '<unknown>'))) @@ -418,7 +458,7 @@ def idle(task, *args, **kwargs): _idleQueue.put_nowait(idleTask) if name is not None: - _idleQueueSet.add(name) + _idleQueueDict[name] = idleTask else: time.sleep(after)