diff --git a/fsl/utils/idle.py b/fsl/utils/idle.py index c1184a31a758b685b476e843140a14ce0b284980..7c4c881063cd2b5c2611d1aac12e2c4335208225 100644 --- a/fsl/utils/idle.py +++ b/fsl/utils/idle.py @@ -391,27 +391,41 @@ def cancelIdle(taskName): _idleQueueDict[taskName].timeout = -1 -def block(secs, delta=0.01): +def block(secs, delta=0.01, until=None): """Blocks for the specified number of seconds, yielding to the main ``wx`` loop. If ``wx`` is not available, or a ``wx`` application is not running, this function is equivalent to ``time.sleep(secs)``. + If ``until`` is provided, this function will block until ``until`` + returns ``True``, or ``secs`` have elapsed, whichever comes first. + :arg secs: Time in seconds to block :arg delta: Time in seconds to sleep between successive yields to ``wx``. + :arg until: Function which returns ``True`` or ``False``, and which + determins when calls to ``block`` will return. """ - from fsl.utils.platform import platform as fslplatform + def defaultUntil(): + return False - if not fslplatform.haveGui: - time.sleep(secs) - else: - import wx - start = time.time() - while (time.time() - start) < secs: + def tick(): + if fslplatform.haveGui: + import wx wx.YieldIfNeeded() - time.sleep(delta) + time.sleep(delta) + + if until is None: + until = defaultUntil + + from fsl.utils.platform import platform as fslplatform + + start = time.time() + while (time.time() - start) < secs: + tick() + if until(): + break def idle(task, *args, **kwargs): diff --git a/fsl/utils/platform.py b/fsl/utils/platform.py index 04ffb98d6b0aafcdd9c2ce6ebd62301b2c091ebd..6d3d31eda42c6f6ae70130bd558c07e9eafed985 100644 --- a/fsl/utils/platform.py +++ b/fsl/utils/platform.py @@ -163,9 +163,27 @@ class Platform(notifier.Notifier): try: import wx app = wx.GetApp() + + # TODO Previously this conditional + # also used app.IsMainLoopRunning() + # to check that the wx main loop + # was running. But this doesn't + # suit situations where a non-main + # event loop is running (e.g. when + # the event loop is being run by + # IPython). + # + # In c++ wx, there is the + # wx.App.UsesEventLoop method, but + # this is not presently exposed to + # Python code. + # + # So this constraint has been + # (hopefully) temporarily relaxed + # until UsesEventLoop can be called + # from Python. return (self.canHaveGui and - app is not None and - app.IsMainLoopRunning()) + app is not None) except ImportError: return False diff --git a/tests/test_idle.py b/tests/test_idle.py index 84a11b968882cac97ecfec08fcc19406dc2cdf0c..90c28c709c7d681aabb1a811743a9cbebead1498 100644 --- a/tests/test_idle.py +++ b/tests/test_idle.py @@ -184,6 +184,25 @@ def _test_block(): assert called[0] +@pytest.mark.wxtest +def test_block_until_with_gui(): _run_with_wx( _test_block_until) +def test_block_until_without_gui(): _run_without_wx(_test_block_until) +def _test_block_until(): + ev = threading.Event() + + def task(): + time.sleep(1) + ev.set() + + threading.Thread(target=task).start() + + start = time.time() + idle.block(3, until=ev.is_set) + end = time.time() + + assert end - start < 3 + + @pytest.mark.wxtest def test_idle(): diff --git a/tests/test_platform.py b/tests/test_platform.py index a747ac03a3ae0d72fc0bc9397da0aa2a10afd613..6dcbfaeeab8a7f7a1ad04732f6ac810bd30992d0 100644 --- a/tests/test_platform.py +++ b/tests/test_platform.py @@ -59,6 +59,7 @@ def test_haveGui(): wx.CallLater(500, runtest) app.MainLoop() + del app assert passed[0]