diff --git a/fsl/utils/async.py b/fsl/utils/async.py index 08142c82fa670e8be60236cad2c7ab3faa5d996a..938a83c384746540cf77bce76dd089e64591d4aa 100644 --- a/fsl/utils/async.py +++ b/fsl/utils/async.py @@ -96,14 +96,6 @@ except: import Queue as queue log = logging.getLogger(__name__) -def _haveWX(): - """Returns ``True`` if we are running within a ``wx`` application, - ``False`` otherwise. - """ - import fsl.utils.platform as fslplatform - return fslplatform.platform.haveGui - - def run(task, onFinish=None, onError=None, name=None): """Run the given ``task`` in a separate thread. @@ -125,10 +117,12 @@ def run(task, onFinish=None, onError=None, name=None): the return value will be ``None``. """ + from fsl.utils.platform import platform as fslplatform + if name is None: name = getattr(task, '__name__', '<unknown>') - haveWX = _haveWX() + haveWX = fslplatform.haveGui # Calls the onFinish or onError handler def callback(cb, *args, **kwargs): @@ -397,9 +391,11 @@ def idle(task, *args, **kwargs): 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 another call to ``idle`` must - be made after the ``MainLoop`` has started for the - original task to be executed. + 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. @@ -426,6 +422,8 @@ def idle(task, *args, **kwargs): ``alwaysQueue``. """ + from fsl.utils.platform import platform as fslplatform + global _idleRegistered global _idleTimer global _idleQueue @@ -439,13 +437,29 @@ def idle(task, *args, **kwargs): skipIfQueued = kwargs.pop('skipIfQueued', False) alwaysQueue = kwargs.pop('alwaysQueue', False) - havewx = _haveWX() + 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): - if havewx or alwaysQueue: 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') - if havewx and (not _idleRegistered): - app = wx.GetApp() app.Bind(wx.EVT_IDLE, _wxIdleLoop) _idleTimer = wx.Timer(app) @@ -546,12 +560,14 @@ def wait(threads, task, *args, **kwargs): a keyword argument called ``wait_direct``. """ + from fsl.utils.platform import platform as fslplatform + direct = kwargs.pop('wait_direct', False) if not isinstance(threads, collections.Sequence): threads = [threads] - haveWX = _haveWX() + haveWX = fslplatform.haveGui def joinAll(): log.debug('Wait thread joining on all targets') diff --git a/tests/test_async.py b/tests/test_async.py index 9d96801c8f469f356850931bdf877ae5d5c3a4f1..9a2e03d56b0aaf4db299eaf2d694da1636ab0f14 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -9,29 +9,46 @@ import time import threading import random +from six.moves import reload_module + import pytest +import mock import fsl.utils.async as async from fsl.utils.platform import platform as fslplatform - +# We use a single wx.App object because wx.GetApp() +# will still return an old App objectd after its +# mainloop has exited. and therefore async.idle +# will potentially register on EVT_IDLE with the +# wrong wx.App object. +_wxapp = None def _run_with_wx(func, *args, **kwargs): + global _wxapp + propagateRaise = kwargs.pop('propagateRaise', True) startingDelay = kwargs.pop('startingDelay', 500) finishingDelay = kwargs.pop('finishingDelay', 500) - - import wx + callAfterApp = kwargs.pop('callAfterApp', None) + + import wx result = [None] raised = [None] - app = wx.App() + + if _wxapp is None: + _wxapp = wx.App() frame = wx.Frame(None) - + + if callAfterApp is not None: + callAfterApp() + def wrap(): try: - result[0] = func(*args, **kwargs) + if func is not None: + result[0] = func(*args, **kwargs) except Exception as e: print(e) @@ -40,14 +57,14 @@ def _run_with_wx(func, *args, **kwargs): finally: def finish(): frame.Destroy() - app.ExitMainLoop() + _wxapp.ExitMainLoop() wx.CallLater(finishingDelay, finish) frame.Show() wx.CallLater(startingDelay, wrap) - app.MainLoop() + _wxapp.MainLoop() async.idleReset() if raised[0] and propagateRaise: @@ -244,25 +261,98 @@ def test_idle_dropIfQueued(): assert task2called[0] -def test_idle_alwaysQueue(): +def test_idle_alwaysQueue1(): + # Test scheduling the task before + # a wx.App has been created. called = [False] def task(): called[0] = True + # In this scenario, an additional call + # to idle (after the App has been created) + # is necessary, otherwise the originally + # queued task will not be called. def nop(): pass + # The task should be run + # when the mainloop starts async.idle(task, alwaysQueue=True) - # We need to queue another task - # for the first task to be executed + # Second call to async.idle _run_with_wx(async.idle, nop) assert called[0] +def test_idle_alwaysQueue2(): + + # Test scheduling the task + # after a wx.App has been craeted, + # but before MainLoop has started + + called = [False] + + def task(): + called[0] = True + + def queue(): + async.idle(task, alwaysQueue=True) + + _run_with_wx(None, callAfterApp=queue) + + assert called[0] + + +def test_idle_alwaysQueue3(): + + # Test scheduling the task + # after a wx.App has been craeted + # and the MainLoop has started. + # In this case, alwaysQueue should + # have no effect - the task should + # just be queued and executed as + # normal. + + called = [False] + + def task(): + called[0] = True + + _run_with_wx(async.idle, task, alwaysQueue=True) + + assert called[0] + + +def test_idle_alwaysQueue4(): + + # Test scheduling the task when + # wx is not present - the task + # should just be executed immediately + called = [False] + + def task(): + called[0] = True + + import fsl.utils.platform + with mock.patch.dict('sys.modules', {'wx' : None}): + + # async uses the platform module to + # determine whether a GUI is available, + # so we have to reload it + reload_module(fsl.utils.platform) + async.idle(task, alwaysQueue=True) + + with pytest.raises(ImportError): + import wx + + reload_module(fsl.utils.platform) + + assert called[0] + + def test_idle_timeout(): called = [False]