Skip to content
Snippets Groups Projects
Commit 0d07fa79 authored by Paul McCarthy's avatar Paul McCarthy :mountain_bicyclist:
Browse files

RF: Reorganised idle loop logic into a single class. Deprecated various

module-level functions. No changes to functionality
parent 47067c1d
No related branches found
No related tags found
No related merge requests found
...@@ -8,9 +8,9 @@ ...@@ -8,9 +8,9 @@
asynchronously, either in an idle loop, or on a separate thread. asynchronously, either in an idle loop, or on a separate thread.
.. note:: The *idle* functions in this module are intended to be run from .. note:: The :class:`IdleLoop` functionality in this module is intended to be
within a ``wx`` application. However, they will still work without run from within a ``wx`` application. However, it will still work
``wx``, albeit with slightly modified behaviour. without ``wx``, albeit with slightly modified behaviour.
Idle tasks Idle tasks
...@@ -19,28 +19,20 @@ Idle tasks ...@@ -19,28 +19,20 @@ Idle tasks
.. autosummary:: .. autosummary::
:nosignatures: :nosignatures:
block IdleLoop
idle idle
idleWhen idleWhen
inIdle block
cancelIdle
idleReset
getIdleTimeout
setIdleTimeout
The :func:`idle` function is a simple way to run a task on an ``wx``
``EVT_IDLE`` event handler. This effectively performs the same job as the
:func:`run` function, but is more suitable for short tasks which do not
warrant running in a separate thread.
The :class:`IdleLoop` class provides a simple way to run a task on an ``wx``
``EVT_IDLE`` event handler. A single ``IdleLoop`` instance is created when
this module is imported; it can be accessed via the :attr:`idleLoop` attribute,
and via the module-level :func:`idle` and :func:`idleWhen` functions.
The ``EVT_IDLE`` event is generated automatically by ``wx``. However, there The :meth:`IdleLoop.idle` method effectively performs the same job as the
are some circumstances in which ``EVT_IDLE`` will not be generated, and :func:`run` function (described below), but is more suitable for short tasks
pending events may be left on the queue. For this reason, the which do not warrant running in a separate thread.
:func:`_wxIdleLoop` will occasionally use a ``wx.Timer`` to ensure that it
continues to be called. The time-out used by this ``Timer`` can be queried
and set via the :func:`getIdleTimeout` and :func:`setIdleTimeout` functions.
Thread tasks Thread tasks
...@@ -58,9 +50,10 @@ The :func:`run` function simply runs a task in a separate thread. This ...@@ -58,9 +50,10 @@ The :func:`run` function simply runs a task in a separate thread. This
doesn't seem like a worthy task to have a function of its own, but the doesn't seem like a worthy task to have a function of its own, but the
:func:`run` function additionally provides the ability to schedule another :func:`run` function additionally provides the ability to schedule another
function to run on the ``wx.MainLoop`` when the original function has function to run on the ``wx.MainLoop`` when the original function has
completed. This therefore gives us a simple way to run a computationally completed (via :func:`idle`). This therefore gives us a simple way to run a
intensitve task off the main GUI thread (preventing the GUI from locking up), computationally intensitve task off the main GUI thread (preventing the GUI
and to perform some clean up/refresh afterwards. from locking up), and to perform some clean up/refresh/notification
afterwards.
The :func:`wait` function is given one or more ``Thread`` instances, and a The :func:`wait` function is given one or more ``Thread`` instances, and a
...@@ -91,304 +84,512 @@ from collections import abc ...@@ -91,304 +84,512 @@ from collections import abc
try: import queue try: import queue
except ImportError: import Queue as queue except ImportError: import Queue as queue
from fsl.utils.deprecated import deprecated
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def run(task, onFinish=None, onError=None, name=None): class IdleTask(object):
"""Run the given ``task`` in a separate thread. """Container object used by the :class:`IdleLoop` class.
Used to encapsulate information about a queued task.
"""
:arg task: The function to run. Must accept no arguments. def __init__(self,
name,
task,
schedtime,
after,
timeout,
args,
kwargs):
self.name = name
self.task = task
self.schedtime = schedtime
self.after = after
self.timeout = timeout
self.args = args
self.kwargs = kwargs
:arg onFinish: An optional function to schedule (on the ``wx.MainLoop``,
via :func:`idle`) once the ``task`` has finished.
:arg onError: An optional function to be called (on the ``wx.MainLoop``, class IdleLoop(object):
via :func:`idle`) if the ``task`` raises an error. Passed """This class contains logic for running tasks via ``wx.EVT_IDLE`` events.
the ``Exception`` that was raised.
:arg name: An optional name to use for this task in log statements. A single ``IdleLoop`` instance is created when this module is first
imported - it is accessed via the module-level :attr:`idleLoop` attribute.
:returns: A reference to the ``Thread`` that was created. In normal circumstances, this ``idleLoop`` instance should be treated as a
singleton, although this is not enforced in any way.
.. note:: If a ``wx`` application is not running, the ``task`` and The ``EVT_IDLE`` event is generated automatically by ``wx`` during periods
``onFinish`` functions will simply be called directly, and of inactivity. However, there are some circumstances in which ``EVT_IDLE``
the return value will be ``None``. will not be generated, and pending events may be left on the queue. For
this reason, the ``IdleLoop`` will occasionally use a ``wx.Timer`` to
ensure that it continues to be called. The time-out used by this ``Timer``
can be queried and set via the :meth:`callRate` property.
""" """
from fsl.utils.platform import platform as fslplatform def __init__(self):
"""Create an ``IdleLoop``.
if name is None: This method does not do much - the real initialisation takes place
name = getattr(task, '__name__', '<unknown>') on the first call to :meth:`idle`.
"""
self.__registered = False
self.__queue = queue.Queue()
self.__queueDict = {}
self.__timer = None
self.__callRate = 200
self.__allowErrors = False
# Call reset on exit, in case
# the idle.timer is active.
atexit.register(self.reset)
@property
def registered(self):
"""Boolean flag indicating whether a handler has been registered on
``wx.EVT_IDLE`` events. Checked and set in the :meth:`idle` method.
"""
return self.__registered
haveWX = fslplatform.haveGui
# Calls the onFinish or onError handler @property
def callback(cb, *args, **kwargs): def queue(self):
"""A ``Queue`` of functions which are to be run on the ``wx.EVT_IDLE``
loop.
"""
return self.__queue
if cb is None:
return
if haveWX: idle(cb, *args, **kwargs) @property
else: cb( *args, **kwargs) def queueDict(self):
"""A ``dict`` containing the names of all named tasks which are
currently queued on the idle loop (see the ``name`` parameter to the
:meth:`idle` method).
"""
return self.__queueDict
# Runs the task, and calls
# callback functions as needed.
def wrapper():
try: @property
task() def timer(self):
log.debug('Task "{}" finished'.format(name)) """A ``wx.Timer`` instance which is used to periodically trigger the
callback(onFinish) :func:`_wxIdleLoop` in circumstances where ``wx.EVT_IDLE`` events may
not be generated. This is created in the first call to :meth:`idle`.
"""
return self.__timer
except Exception as e:
log.warning('Task "{}" crashed'.format(name), exc_info=True) @property
callback(onError, e) def callRate(self):
"""Minimum time (in milliseconds) between consecutive calls to the idle
loop (:meth:`__idleLoop`). If ``wx.EVT_IDLE`` events are not being
fired, the :meth:`timer` is used to maintain the idle loop at this
rate.
"""
return self.__callRate
# If WX, run on a thread
if haveWX:
log.debug('Running task "{}" on thread'.format(name)) @callRate.setter
def callRate(self, rate):
"""Update the :meth:`callRate` to ``rate`` (specified in milliseconds).
thread = threading.Thread(target=wrapper) If ``rate is None``, it is set to the default of 200 milliseconds.
thread.start() """
return thread
# Otherwise run directly if rate is None:
else: rate = 200
log.debug('Running task "{}" directly'.format(name))
wrapper()
return None
log.debug('Idle loop timeout changed to {}'.format(rate))
_idleRegistered = False self.__callRate = rate
"""Boolean flag indicating whether the :func:`_wxIdleLoop` function has
been registered as a ``wx.EVT_IDLE`` event handler. Checked and set
in the :func:`idle` function.
"""
_idleQueue = queue.Queue() @property
"""A ``Queue`` of functions which are to be run on the ``wx.EVT_IDLE`` def allowErrors(self):
loop. """Used for testing/debugging. If ``True``, and a function called on
""" the idle loop raises an error, that error will not be caught, and the
idle loop will stop.
"""
return self.__allowErrors
_idleQueueDict = {} @allowErrors.setter
"""A ``dict`` containing the names of all named tasks which are def allowErrors(self, allow):
currently queued on the idle loop (see the ``name`` parameter to the """Update the ``allowErrors`` flag. """
:func:`idle` function). self.__allowErrors = allow
"""
_idleTimer = None def reset(self):
"""A ``wx.Timer`` instance which is used to periodically trigger the """Reset the internal idle loop state.
:func:`_wxIdleLoop` in circumstances where ``wx.EVT_IDLE`` events may not
be generated. This is created in the first call to :func:`idle`.
"""
In a normal execution environment, this method will never need to be
called. However, in an execution environment where multiple ``wx.App``
instances are created, run, and destroyed sequentially, this function
will need to be called after each ``wx.App`` has been destroyed.
Otherwise the ``idle`` function will not work during exeution of
subsequent ``wx.App`` instances.
"""
_idleCallRate = 200 if self.__timer is not None:
"""Minimum time (in milliseconds) between consecutive calls to self.__timer.Stop()
:func:`_wxIdleLoop`. If ``wx.EVT_IDLE`` events are not being fired, the
:attr:`_idleTimer` is used to maintain the idle loop at this rate.
"""
# If we're atexit, the ref to
# the queue module might have
# been cleared, in which case
# we don't want to create a
# new one.
if self.__queue is not None: newQueue = queue.Queue()
else: newQueue = None
_idleAllowErrors = False self.__registered = False
"""Used for testing/debugging. If ``True``, and a function called on the idle self.__queue = newQueue
loop raises an error, that error will not be caught, and the idle loop will self.__queueDict = {}
stop. self.__timer = None
""" self.__callRate = 200
self.__allowErrors = False
def idleReset(): def inIdle(self, taskName):
"""Reset the internal :func:`idle` queue state. """Returns ``True`` if a task with the given name is queued on the
idle loop (or is currently running), ``False`` otherwise.
In a normal execution environment, this function will never need to be """
called. However, in an execution environment where multiple ``wx.App`` return taskName in self.__queueDict
instances are created, run, and destroyed sequentially, this function
will need to be called after each ``wx.App`` has been destroyed.
Otherwise the ``idle`` function will not work during exeution of
subsequent ``wx.App`` instances.
"""
global _idleRegistered
global _idleQueue
global _idleQueueDict
global _idleTimer
global _idleCallRate
global _idleAllowErrors
if _idleTimer is not None:
_idleTimer.Stop()
# If we're atexit, the ref def cancelIdle(self, taskName):
# to the queue module might """If a task with the given ``taskName`` is in the idle queue, it
# have been cleared. is cancelled. If the task is already running, it cannot be cancelled.
if queue is not None: newQueue = queue.Queue()
else: newQueue = None
_idleRegistered = False A ``KeyError`` is raised if no task called ``taskName`` exists.
_idleQueue = newQueue """
_idleQueueDict = {} self.__queueDict[taskName].timeout = -1
_idleTimer = None
_idleCallRate = 200
_idleAllowErrors = False
# Call idleReset on exit, in def idle(self, task, *args, **kwargs):
# case the idleTimer is active. """Run the given task on a ``wx.EVT_IDLE`` event.
atexit.register(idleReset)
:arg task: The task to run.
def getIdleTimeout(): :arg name: Optional. If provided, must be provided as a keyword
"""Returns the current ``wx`` idle loop time out/call rate. argument. Specifies a name that can be used to
""" query the state of this task via :meth:`inIdle`.
return _idleCallRate
:arg after: Optional. If provided, must be provided as a keyword
argument. A time, in seconds, which specifies the
amount of time to wait before running this task
after it has been scheduled.
def setIdleTimeout(timeout=None): :arg timeout: Optional. If provided, must be provided as a keyword
"""Set the ``wx`` idle loop time out/call rate. If ``timeout`` is not argument. Specifies a time out, in seconds. If this
provided, or is set to ``None``, the timeout is set to 200 milliseconds. amount of time passes before the function gets
""" scheduled to be called on the idle loop, the
function is not called, and is dropped from the
queue.
global _idleCallRate :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.
if timeout is None: :arg skipIfQueued: Optional. If provided, must be provided as a keyword
timeout = 200 argument. If ``True``, and a task with the given
``name`` is already enqueud, (or is running), the
function is not called. Defaults to ``False``.
log.debug('Idle loop timeout changed to {}'.format(timeout)) :arg alwaysQueue: Optional. If provided, must be provided as a keyword
argument. If ``True``, and a ``wx.MainLoop`` is not
running, the task is enqueued anyway, under the
assumption that a ``wx.MainLoop`` will be started in
the future. Note that, if ``wx.App`` has not yet
been created, another call to ``idle`` must be made
after the app has been created for the original task
to be executed. If ``wx`` is not available, this
parameter will be ignored, and the task executed
directly.
_idleCallRate = timeout
All other arguments are passed through to the task function.
class IdleTask(object):
"""Container object used by the :func:`idle` and :func:`_wxIdleLoop`
functions.
"""
def __init__(self, If a ``wx.App`` is not running, the ``timeout``, ``name`` and
name, ``skipIfQueued`` arguments are ignored. Instead, the call will sleep
task, for ``after`` seconds, and then the ``task`` is called directly.
schedtime,
after,
timeout,
args,
kwargs):
self.name = name
self.task = task
self.schedtime = schedtime
self.after = after
self.timeout = timeout
self.args = args
self.kwargs = kwargs
def _wxIdleLoop(ev): .. note:: If the ``after`` argument is used, there is no guarantee that
"""Function which is called on ``wx.EVT_IDLE`` events, and occasionally the task will be executed in the order that it is scheduled.
on ``wx.EVT_TIMER`` events via the :attr:`_idleTimer`. If there This is because, if the required time has not elapsed when
is a function on the :attr:`_idleQueue`, it is popped and called. the task is popped from the queue, it will be re-queued.
.. note:: The ``wx.EVT_IDLE`` event is only triggered on user interaction .. note:: If you schedule multiple tasks with the same ``name``, and
(e.g. mouse movement). This means that a situation may arise you do not use the ``skipIfQueued`` or ``dropIfQueued``
whereby a function is queued via the :func:`idle` function, but arguments, all of those tasks will be executed, but you will
no ``EVT_IDLE`` event gets generated. Therefore, the only be able to query/cancel the most recently enqueued
:attr:`_idleTimer` object is occasionally used to call this task.
function as well.
""" .. note:: You will run into difficulties if you schedule a function
that expects/accepts its own keyword arguments called
``name``, ``skipIfQueued``, ``dropIfQueued``, ``after``,
``timeout``, or ``alwaysQueue``.
"""
from fsl.utils.platform import platform as fslplatform
schedtime = time.time()
timeout = kwargs.pop('timeout', 0)
after = kwargs.pop('after', 0)
name = kwargs.pop('name', None)
dropIfQueued = kwargs.pop('dropIfQueued', False)
skipIfQueued = kwargs.pop('skipIfQueued', False)
alwaysQueue = kwargs.pop('alwaysQueue', False)
canHaveGui = fslplatform.canHaveGui
haveGui = fslplatform.haveGui
# If there is no possibility of a
# gui being available in the future
# (determined by canHaveGui), then
# alwaysQueue is ignored.
alwaysQueue = alwaysQueue and canHaveGui
# We don't have wx - run the task
# directly/synchronously.
if not (haveGui or alwaysQueue):
time.sleep(after)
log.debug('Running idle task directly')
task(*args, **kwargs)
return
import wx
app = wx.GetApp()
# Register on the idle event
# if an app is available
#
# n.b. The 'app is not None' test will
# potentially fail in scenarios where
# multiple wx.Apps have been instantiated,
# as it may return a previously created
# app that is no longer active.
if (not self.registered) and (app is not None):
log.debug('Registering async idle loop')
app.Bind(wx.EVT_IDLE, self.__idleLoop)
# We also occasionally use a
# timer to drive the loop, so
# let's register that as well
self.__timer = wx.Timer(app)
self.__timer.Bind(wx.EVT_TIMER, self.__idleLoop)
self.__registered = True
# A task with the specified
# name is already in the queue
if name is not None and self.inIdle(name):
# Drop the old task
# with the same name
if dropIfQueued:
import wx # The cancelIdle function sets the old
global _idleQueue # task timeout to -1, so it won't get
global _idleQueueDict # executed. But the task is left in the
global _idleTimer # queue, and in the queueDict.
global _idleCallRate # In the latter, the old task gets
global _idleAllowErrors # overwritten with the new task below.
self.cancelIdle(name)
ev.Skip() log.debug('Idle task ({}) is already queued - '
'dropping the old task'.format(name))
try:
task = _idleQueue.get_nowait() # Ignore the new task
# with the same name
except queue.Empty: elif skipIfQueued:
log.debug('Idle task ({}) is already queued '
# Make sure that we get called periodically, '- skipping it'.format(name))
# if EVT_IDLE decides to stop firing. If return
# _idleTimer is None, then idleReset has
# probably been called. log.debug('Scheduling idle task ({}) on wx idle '
if _idleTimer is not None: 'loop'.format(getattr(task, '__name__', '<unknown>')))
_idleTimer.Start(_idleCallRate, wx.TIMER_ONE_SHOT)
return idleTask = IdleTask(name,
task,
now = time.time() schedtime,
elapsed = now - task.schedtime after,
queueSizeOffset = 0 timeout,
taskName = task.name args,
funcName = getattr(task.task, '__name__', '<unknown>') kwargs)
if taskName is None: taskName = funcName self.__queue.put_nowait(idleTask)
else: taskName = '{} [{}]'.format(taskName, funcName)
if name is not None:
# Has enough time elapsed self.__queueDict[name] = idleTask
# since the task was scheduled?
# If not, re-queue the task.
# If this is the only task on the def idleWhen(self, func, condition, *args, **kwargs):
# queue, the idle loop will be """Poll the ``condition`` function periodically, and schedule ``func``
# called again after on :meth:`idle` when it returns ``True``.
# _idleCallRate millisecs.
if elapsed < task.after: :arg func: Function to call.
log.debug('Re-queueing function ({}) on wx idle loop'.format(taskName))
_idleQueue.put_nowait(task) :arg condition: Function which returns ``True`` or ``False``. The
queueSizeOffset = 1 ``func`` function is only called when the
``condition`` function returns ``True``.
# Has the task timed out?
elif task.timeout == 0 or (elapsed < task.timeout): :arg pollTime: Must be passed as a keyword argument. Time (in seconds)
to wait between successive calls to ``when``. Defaults
log.debug('Running function ({}) on wx idle loop'.format(taskName)) to ``0.2``.
"""
pollTime = kwargs.get('pollTime', 0.2)
if not condition():
self.idle(self.idleWhen,
func,
condition,
after=pollTime,
*args,
**dict(kwargs))
else:
kwargs.pop('pollTime', None)
self.idle(func, *args, **kwargs)
def __idleLoop(self, ev):
"""This method is called on ``wx.EVT_IDLE`` events, and occasionally
on ``wx.EVT_TIMER`` events via the :meth:`timer`. If there
is a function on the :meth:`queue`, it is popped and called.
.. note:: The ``wx.EVT_IDLE`` event is only triggered on user
interaction (e.g. mouse movement). This means that a
situation may arise whereby a function is queued via the
:meth:`idle` method, but no ``EVT_IDLE`` event gets
generated. Therefore, the :meth:`timer` object is
occasionally used to call this function as well.
"""
import wx
ev.Skip()
try: try:
task.task(*task.args, **task.kwargs) task = self.__queue.get_nowait()
except Exception as e:
log.warning('Idle task {} crashed - {}: {}'.format(
taskName, type(e).__name__, str(e)), exc_info=True)
if _idleAllowErrors: except queue.Empty:
raise e
if task.name is not None: # Make sure that we get called periodically,
try: _idleQueueDict.pop(task.name) # if EVT_IDLE decides to stop firing. If
except KeyError: pass # self.timer is None, then self.reset has
# probably been called.
if self.__timer is not None:
self.__timer.Start(self.__callRate, wx.TIMER_ONE_SHOT)
return
# More tasks on the queue? now = time.time()
# Request anotherd event elapsed = now - task.schedtime
if _idleQueue.qsize() > queueSizeOffset: queueSizeOffset = 0
ev.RequestMore() taskName = task.name
funcName = getattr(task.task, '__name__', '<unknown>')
if taskName is None: taskName = funcName
else: taskName = '{} [{}]'.format(taskName, funcName)
# Has enough time elapsed
# since the task was scheduled?
# If not, re-queue the task.
# If this is the only task on the
# queue, the idle loop will be
# called again after
# callRate millisecs.
if elapsed < task.after:
log.debug('Re-queueing function ({}) on '
'wx idle loop'.format(taskName))
self.__queue.put_nowait(task)
queueSizeOffset = 1
# Has the task timed out?
elif task.timeout == 0 or (elapsed < task.timeout):
log.debug('Running function ({}) on wx '
'idle loop'.format(taskName))
# Otherwise use the idle try:
# timer to make sure that task.task(*task.args, **task.kwargs)
# the loop keeps ticking except Exception as e:
# over log.warning('Idle task {} crashed - {}: {}'.format(
else: taskName, type(e).__name__, str(e)), exc_info=True)
_idleTimer.Start(_idleCallRate, wx.TIMER_ONE_SHOT)
if self.__allowErrors:
raise e
def inIdle(taskName): if task.name is not None:
"""Returns ``True`` if a task with the given name is queued on the try: self.__queueDict.pop(task.name)
idle loop (or is currently running), ``False`` otherwise. except KeyError: pass
# More tasks on the queue?
# Request anotherd event
if self.__queue.qsize() > queueSizeOffset:
ev.RequestMore()
# Otherwise use the idle
# timer to make sure that
# the loop keeps ticking
# over
else:
self.__timer.Start(self.__callRate, wx.TIMER_ONE_SHOT)
idleLoop = IdleLoop()
"""A singleton :class:`IdleLoop` instance, created when this module is
imported.
"""
def idle(*args, **kwargs):
"""Equivalent to calling :meth:`IdleLoop.idle` on the ``idleLoop``
singleton.
""" """
global _idleQueueDict idleLoop.idle(*args, **kwargs)
return taskName in _idleQueueDict
def idleWhen(*args, **kwargs):
"""Equivalent to calling :meth:`IdleLoop.idleWhen` on the ``idleLoop``
singleton.
"""
idleLoop.idleWhen(*args, **kwargs)
@deprecated('2.7.0', '3.0.0', 'Use idleLoop.inIdle instead')
def inIdle(taskName):
"""Deprecated - use ``idleLoop.inIdle`` instead. """
return idleLoop.inIdle(taskName)
@deprecated('2.7.0', '3.0.0', 'Use idleLoop.cancelIdle instead')
def cancelIdle(taskName): def cancelIdle(taskName):
"""If a task with the given ``taskName`` is in the idle queue, it """Deprecated - use ``idleLoop.cancelIdle`` instead. """
is cancelled. If the task is already running, it cannot be cancelled. return idleLoop.cancelIdle(taskName)
@deprecated('2.7.0', '3.0.0', 'Use idleLoop.reset instead')
def idleReset():
"""Deprecated - use ``idleLoop.reset`` instead. """
return idleLoop.reset()
@deprecated('2.7.0', '3.0.0', 'Use idleLoop.callRate instead')
def getIdleTimeout():
"""Deprecated - use ``idleLoop.callRate`` instead. """
return idleLoop.callRate
A ``KeyError`` is raised if no task called ``taskName`` exists.
"""
global _idleQueueDict @deprecated('2.7.0', '3.0.0', 'Use idleLoop.callRate instead')
_idleQueueDict[taskName].timeout = -1 def setIdleTimeout(timeout=None):
"""Deprecated - use ``idleLoop.callRate`` instead. """
idleLoop.callRate = timeout
def block(secs, delta=0.01, until=None): def block(secs, delta=0.01, until=None):
...@@ -428,181 +629,71 @@ def block(secs, delta=0.01, until=None): ...@@ -428,181 +629,71 @@ def block(secs, delta=0.01, until=None):
break break
def idle(task, *args, **kwargs): def run(task, onFinish=None, onError=None, name=None):
"""Run the given task on a ``wx.EVT_IDLE`` event. """Run the given ``task`` in a separate thread.
:arg task: The task to run.
:arg name: Optional. If provided, must be provided as a keyword
argument. Specifies a name that can be used to query
the state of this task via the :func:`inIdle` function.
:arg after: Optional. If provided, must be provided as a keyword
argument. A time, in seconds, which specifies the
amount of time to wait before running this task after
it has been scheduled.
:arg timeout: Optional. If provided, must be provided as a keyword
argument. Specifies a time out, in seconds. If this
amount of time passes before the function gets
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
function is not called. Defaults to ``False``.
:arg alwaysQueue: Optional. If provided, must be provided as a keyword
argument. If ``True``, and a ``wx.MainLoop`` is not
running, the task is enqueued anyway, under the
assumption that a ``wx.MainLoop`` will be started in
the future. Note that, if ``wx.App`` has not yet been
created, another call to ``idle`` must be made after
the app has been created for the original task to be
executed. If ``wx`` is not available, this parameter
will be ignored, and the task executed directly.
All other arguments are passed through to the task function.
:arg task: The function to run. Must accept no arguments.
If a ``wx.App`` is not running, the ``timeout``, ``name`` and :arg onFinish: An optional function to schedule (on the ``wx.MainLoop``,
``skipIfQueued`` arguments are ignored. Instead, the call will sleep for via :func:`idle`) once the ``task`` has finished.
``after`` seconds, and then the ``task`` is called directly.
:arg onError: An optional function to be called (on the ``wx.MainLoop``,
via :func:`idle`) if the ``task`` raises an error. Passed
the ``Exception`` that was raised.
.. note:: If the ``after`` argument is used, there is no guarantee that :arg name: An optional name to use for this task in log statements.
the task will be executed in the order that it is scheduled.
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 :returns: A reference to the ``Thread`` that was created.
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 .. note:: If a ``wx`` application is not running, the ``task`` and
expects/accepts its own keyword arguments called ``name``, ``onFinish`` functions will simply be called directly, and
``skipIfQueued``, ``dropIfQueued``, ``after``, ``timeout``, or the return value will be ``None``.
``alwaysQueue``.
""" """
from fsl.utils.platform import platform as fslplatform from fsl.utils.platform import platform as fslplatform
global _idleRegistered if name is None:
global _idleTimer name = getattr(task, '__name__', '<unknown>')
global _idleQueue
global _idleQueueDict
schedtime = time.time()
timeout = kwargs.pop('timeout', 0)
after = kwargs.pop('after', 0)
name = kwargs.pop('name', None)
dropIfQueued = kwargs.pop('dropIfQueued', False)
skipIfQueued = kwargs.pop('skipIfQueued', False)
alwaysQueue = kwargs.pop('alwaysQueue', False)
canHaveGui = fslplatform.canHaveGui
haveGui = fslplatform.haveGui
# If there is no possibility of a
# gui being available in the future,
# then alwaysQueue is ignored.
if haveGui or (alwaysQueue and canHaveGui):
import wx
app = wx.GetApp()
# Register on the idle event
# if an app is available
#
# n.b. The 'app is not None' test will
# potentially fail in scenarios where
# multiple wx.Apps have been instantiated,
# as it may return a previously created
# app.
if (not _idleRegistered) and (app is not None):
log.debug('Registering async idle loop')
app.Bind(wx.EVT_IDLE, _wxIdleLoop)
_idleTimer = wx.Timer(app)
_idleRegistered = True
_idleTimer.Bind(wx.EVT_TIMER, _wxIdleLoop)
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 ' haveWX = fslplatform.haveGui
'loop'.format(getattr(task, '__name__', '<unknown>')))
idleTask = IdleTask(name, # Calls the onFinish or onError handler
task, def callback(cb, *args, **kwargs):
schedtime,
after,
timeout,
args,
kwargs)
_idleQueue.put_nowait(idleTask) if cb is None:
return
if name is not None: if haveWX: idle(cb, *args, **kwargs)
_idleQueueDict[name] = idleTask else: cb( *args, **kwargs)
else: # Runs the task, and calls
time.sleep(after) # callback functions as needed.
log.debug('Running idle task directly') def wrapper():
task(*args, **kwargs)
try:
task()
log.debug('Task "{}" finished'.format(name))
callback(onFinish)
def idleWhen(func, condition, *args, **kwargs): except Exception as e:
"""Poll the ``condition`` function periodically, and schedule ``func`` on
:func:`idle` when it returns ``True``.
:arg func: Function to call. log.warning('Task "{}" crashed'.format(name), exc_info=True)
callback(onError, e)
:arg condition: Function which returns ``True`` or ``False``. The ``func`` # If WX, run on a thread
function is only called when the ``condition`` function if haveWX:
returns ``True``.
:arg pollTime: Must be passed as a keyword argument. Time (in seconds) to log.debug('Running task "{}" on thread'.format(name))
wait between successive calls to ``when``. Defaults to
``0.2``.
"""
pollTime = kwargs.get('pollTime', 0.2) thread = threading.Thread(target=wrapper)
thread.start()
return thread
if not condition(): # Otherwise run directly
idle(idleWhen, func, condition, after=pollTime, *args, **dict(kwargs))
else: else:
kwargs.pop('pollTime', None) log.debug('Running task "{}" directly'.format(name))
idle(func, *args, **kwargs) wrapper()
return None
def wait(threads, task, *args, **kwargs): def wait(threads, task, *args, **kwargs):
......
...@@ -159,7 +159,14 @@ class Platform(notifier.Notifier): ...@@ -159,7 +159,14 @@ class Platform(notifier.Notifier):
@property @property
def haveGui(self): def haveGui(self):
"""``True`` if we are running with a GUI, ``False`` otherwise. """ """``True`` if we are running with a GUI, ``False`` otherwise.
This currently equates to testing whether a display is available
(see :meth:`canHaveGui`) and whether a ``wx.App`` exists. It
previously also tested whether an event loop was running, but this
is not compatible with execution from IPython/Jupyter notebook, where
the event loop is called periodically, and so is not always running.
"""
try: try:
import wx import wx
app = wx.GetApp() app = wx.GetApp()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment