diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8407a74939c4934079eb04022caeb85a1d307ea6..50be73b05b0cbc9974a2bebf6eaab71ec941dcb2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -156,10 +156,6 @@ variables: script: - bash ./.ci/test_template.sh -test:3.6: - stage: test - image: pauldmccarthy/fsleyes-py36-wxpy4-gtk3 - <<: *test_template test:3.7: stage: test diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6065e4aca6688e9ba0cb0ca5acba552bcc4ced6d..14cc6e7d23f0895984759b962a6047e6f662f284 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,18 @@ Changed specified, which is run on the :mod:`.idle` loop. +Deprecated +^^^^^^^^^^ + + +* Deprecated a number of GUI-specific properties in the + :mod:`fsl.utils.platform` module, including ``frozen``, ``haveGui``, + ``canHaveGui``, ``inSSHSession``, ``inVNCSession``, ``wxPlatform``, + ``wxFlavour``, ``glVersion``, ``glRenderer``, and ``glIsSoftwareRenderer``. + Equivalent functions are being added to the ``fsleyes-widgets`` library. + + + 3.5.3 (Tuesday 9th February 2021) --------------------------------- diff --git a/fsl/data/image.py b/fsl/data/image.py index 7c2bdbcba722e8f0017ce0bba46269283f8e4d8b..0dbd331af452c1e2c23beeaca7a4ea2b77c29672 100644 --- a/fsl/data/image.py +++ b/fsl/data/image.py @@ -1206,7 +1206,8 @@ class Image(Nifti): def __del__(self): """Closes any open file handles, and clears some references. """ - Nifti.__del__(self) + if Nifti is not None: + Nifti.__del__(self) self.__nibImage = None self.__imageWrapper = None diff --git a/fsl/utils/idle.py b/fsl/utils/idle.py index f8ff25349c3b3bc01465dfb9a1ebba584abf543d..dfdb8fec6274b05a05d927d04360d78b5e5cf059 100644 --- a/fsl/utils/idle.py +++ b/fsl/utils/idle.py @@ -89,7 +89,35 @@ except ImportError: import Queue as queue log = logging.getLogger(__name__) -class IdleTask(object): +@functools.lru_cache() +def _canHaveGui(): + """Return ``True`` if wxPython is installed, and a display is available, + ``False`` otherwise. + """ + # Determine if a display is available. We do + # this once at init (instead of on-demand in + # the canHaveGui method) because calling the + # IsDisplayAvailable function will cause the + # application to steal focus under OSX! + try: + import wx + return wx.App.IsDisplayAvailable() + except ImportError: + return False + + +def _haveGui(): + """Return ``True`` if wxPython is installed, a display is available, and + a ``wx.App`` exists, ``False`` otherwise. + """ + try: + import wx + return _canHaveGui() and (wx.GetApp() is not None) + except ImportError: + return False + + +class IdleTask: """Container object used by the :class:`IdleLoop` class. Used to encapsulate information about a queued task. """ @@ -111,7 +139,7 @@ class IdleTask(object): self.kwargs = kwargs -class IdleLoop(object): +class IdleLoop: """This class contains logic for running tasks via ``wx.EVT_IDLE`` events. A single ``IdleLoop`` instance is created when this module is first @@ -370,8 +398,6 @@ class IdleLoop(object): ``timeout``, or ``alwaysQueue``. """ - from fsl.utils.platform import platform as fslplatform - schedtime = time.time() timeout = kwargs.pop('timeout', 0) after = kwargs.pop('after', 0) @@ -380,18 +406,15 @@ class IdleLoop(object): 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 + # (determined by _canHaveGui), then # alwaysQueue is ignored. - alwaysQueue = alwaysQueue and canHaveGui + alwaysQueue = alwaysQueue and _canHaveGui() # We don't have wx - run the task # directly/synchronously. - if self.__neverQueue or not (haveGui or alwaysQueue): + if self.__neverQueue or not (_haveGui() or alwaysQueue): time.sleep(after) log.debug('Running idle task directly') task(*args, **kwargs) @@ -611,11 +634,13 @@ def block(secs, delta=0.01, until=None): determins when calls to ``block`` will return. """ + havewx = _haveGui() + def defaultUntil(): return False def tick(): - if fslplatform.haveGui: + if havewx: import wx wx.YieldIfNeeded() time.sleep(delta) @@ -623,8 +648,6 @@ def block(secs, delta=0.01, until=None): if until is None: until = defaultUntil - from fsl.utils.platform import platform as fslplatform - start = time.time() while (time.time() - start) < secs: tick() @@ -653,12 +676,11 @@ 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 = fslplatform.haveGui + haveWX = _haveGui() # Calls the onFinish or onError handler def callback(cb, *args, **kwargs): @@ -727,14 +749,12 @@ 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, abc.Sequence): threads = [threads] - haveWX = fslplatform.haveGui + haveWX = _haveGui() def joinAll(): log.debug('Wait thread joining on all targets') @@ -755,7 +775,7 @@ def wait(threads, task, *args, **kwargs): return None -class Task(object): +class Task: """Container object which encapsulates a task that is run by a :class:`TaskThread`. """ @@ -775,7 +795,6 @@ class TaskThreadVeto(Exception): handler (if one has been specified). See the :meth:`TaskThread.enqueue` method for more details. """ - pass class TaskThread(threading.Thread): @@ -1005,7 +1024,7 @@ def mutex(*args, **kwargs): return MutexFactory(*args, **kwargs) -class MutexFactory(object): +class MutexFactory: """The ``MutexFactory`` is a placeholder for methods which have been decorated with the :func:`mutex` decorator. When the method of a class is decorated with ``@mutex``, a ``MutexFactory`` is created. diff --git a/fsl/utils/platform.py b/fsl/utils/platform.py index ebd9478cb79f3d11d5cda0d4bf8a3eeb00c534f7..1a49221ad455cfbfe2dace57d483010c2e89f019 100644 --- a/fsl/utils/platform.py +++ b/fsl/utils/platform.py @@ -18,7 +18,8 @@ import os.path as op import sys import importlib -import fsl.utils.notifier as notifier +import fsl.utils.notifier as notifier +import fsl.utils.deprecated as deprecated # An annoying consequence of using # a system-module name for our own @@ -150,6 +151,10 @@ class Platform(notifier.Notifier): @property + @deprecated.deprecated( + '3.6.0', + '4.0.0', + 'Equivalent functionality is available in fsleyes-widgets.') def frozen(self): """``True`` if we are running in a compiled/frozen application, ``False`` otherwise. @@ -158,6 +163,10 @@ class Platform(notifier.Notifier): @property + @deprecated.deprecated( + '3.6.0', + '4.0.0', + 'Equivalent functionality is available in fsleyes-widgets.') def haveGui(self): """``True`` if we are running with a GUI, ``False`` otherwise. @@ -201,12 +210,20 @@ class Platform(notifier.Notifier): @property + @deprecated.deprecated( + '3.6.0', + '4.0.0', + 'Equivalent functionality is available in fsleyes-widgets.') def canHaveGui(self): """``True`` if it is possible to create a GUI, ``False`` otherwise. """ return self.__canHaveGui @property + @deprecated.deprecated( + '3.6.0', + '4.0.0', + 'Equivalent functionality is available in fsleyes-widgets.') def inSSHSession(self): """``True`` if this application is running over an SSH session, ``False`` otherwise. @@ -215,6 +232,10 @@ class Platform(notifier.Notifier): @property + @deprecated.deprecated( + '3.6.0', + '4.0.0', + 'Equivalent functionality is available in fsleyes-widgets.') def inVNCSession(self): """``True`` if this application is running over a VNC (or similar) session, ``False`` otherwise. Currently, the following remote desktop @@ -228,6 +249,10 @@ class Platform(notifier.Notifier): @property + @deprecated.deprecated( + '3.6.0', + '4.0.0', + 'Equivalent functionality is available in fsleyes-widgets.') def wxPlatform(self): """One of :data:`WX_UNKNOWN`, :data:`WX_MAC_COCOA`, :data:`WX_MAC_CARBON`, or :data:`WX_GTK`, indicating the wx platform. @@ -253,6 +278,10 @@ class Platform(notifier.Notifier): @property + @deprecated.deprecated( + '3.6.0', + '4.0.0', + 'Equivalent functionality is available in fsleyes-widgets.') def wxFlavour(self): """One of :data:`WX_UNKNOWN`, :data:`WX_PYTHON` or :data:`WX_PHOENIX`, indicating the wx flavour. @@ -359,6 +388,10 @@ class Platform(notifier.Notifier): @property + @deprecated.deprecated( + '3.6.0', + '4.0.0', + 'Equivalent functionality is available in fsleyes-widgets.') def glVersion(self): """Returns the available OpenGL version, or ``None`` if it has not been set. @@ -367,12 +400,20 @@ class Platform(notifier.Notifier): @glVersion.setter + @deprecated.deprecated( + '3.6.0', + '4.0.0', + 'Equivalent functionality is available in fsleyes-widgets.') def glVersion(self, value): """Set the available OpenGL version. """ self.__glVersion = value @property + @deprecated.deprecated( + '3.6.0', + '4.0.0', + 'Equivalent functionality is available in fsleyes-widgets.') def glRenderer(self): """Returns the available OpenGL renderer, or ``None`` if it has not been set. @@ -381,6 +422,10 @@ class Platform(notifier.Notifier): @glRenderer.setter + @deprecated.deprecated( + '3.6.0', + '4.0.0', + 'Equivalent functionality is available in fsleyes-widgets.') def glRenderer(self, value): """Set the available OpenGL renderer. """ self.__glRenderer = value @@ -398,6 +443,10 @@ class Platform(notifier.Notifier): @property + @deprecated.deprecated( + '3.6.0', + '4.0.0', + 'Equivalent functionality is available in fsleyes-widgets.') def glIsSoftwareRenderer(self): """Returns ``True`` if the OpenGL renderer is software based, ``False`` otherwise, or ``None`` if the renderer has not yet been set. diff --git a/fsl/utils/settings.py b/fsl/utils/settings.py index ceeb6bfe9060f6f8db8f28987869bc84e22628fc..523caddc184f0f88b9f4069cbcab1f32ba3c7e11 100644 --- a/fsl/utils/settings.py +++ b/fsl/utils/settings.py @@ -421,7 +421,7 @@ class Settings(object): try: with open(configFile, 'wb') as f: pickle.dump(config, f, protocol=2) - except (IOError, pickle.PicklingError, EOFError): + except (IOError, pickle.PicklingError, EOFError, FileNotFoundError): log.warning('Unable to save {} configuration file ' '{}'.format(self.__configID, configFile), exc_info=True) diff --git a/tests/test_idle.py b/tests/test_idle.py index e6615319eaaefcf28b7309a56570f70f8873f991..edf0ecb171b54fe382d233c5c69a49ae77b57f1c 100644 --- a/tests/test_idle.py +++ b/tests/test_idle.py @@ -38,6 +38,11 @@ def _run_with_wx(func, *args, **kwargs): if callAfterApp is not None: callAfterApp() + # canHaveGui caches its return val, + # so clear it otherwise we may + # affect subsequent tests + idle._canHaveGui.cache_clear() + def wrap(): try: @@ -64,6 +69,8 @@ def _run_with_wx(func, *args, **kwargs): idle.idleLoop.reset() + idle._canHaveGui.cache_clear() + if raised[0] and propagateRaise: raise raised[0] @@ -413,10 +420,9 @@ def test_idle_alwaysQueue4(): import fsl.utils.platform with mock.patch.dict('sys.modules', {'wx' : None}): - # idle uses the platform module to - # determine whether a GUI is available, - # so we have to reload it - reload_module(fsl.utils.platform) + # The idle._canHaveGui caches its result, + # so we need to invalidate it + idle._canHaveGui.cache_clear() idle.idle(task, alwaysQueue=True) with pytest.raises(ImportError):