diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 63302ceedba7fac56d09169f04a99a6185972b2b..462a26fd447effd2aed45217753c53636c8c3ade 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,42 @@ This document contains the ``fslpy`` release history in reverse chronological order. +1.9.0 (Under development) +------------------------- + + +Added +^^^^^ + + +* New :meth:`.Image.data` property method, for easy access to image data + as a ``numpy`` array. +* New ``log`` option to the :func:`.run.run` function, allowing more + fine-grained control over sub-process output streams. +* New :meth:`.Platform.fsldevdir` property, allowing the ``$FSLDEVDIR`` + environment variable to be queried/changed. + + +Changed +^^^^^^^ + + +* :meth:`.Image.ndims` has been renamed to :meth:`.Image.ndim`, to align + more closely with ``numpy`` naming conventions. +* The ``err`` and ``ret`` parameters to the :func:`.run.run` function have + been renamed to ``stderr`` and ``exitcode`` respectively. +* The :func:`.runfsl` function will give priority to the ``$FSLDEVDIR`` + environment variable if it is set. + + +Deprecated +^^^^^^^^^^ + + +* :meth:`.Image.ndims`. +* The ``err`` and ``ret`` parameters to :func:`.run.run`. + + 1.8.1 (Friday May 11th 2018) ---------------------------- diff --git a/fsl/data/image.py b/fsl/data/image.py index c03618d856931ee96c0ab037ba530aa42d4c1a76..77889897f2610ac6d5735c81ffff7de85a226f40 100644 --- a/fsl/data/image.py +++ b/fsl/data/image.py @@ -491,7 +491,7 @@ class Nifti(notifier.Notifier, meta.Meta): @property - def ndims(self): + def ndim(self): """Returns the number of dimensions in this image. This number may not match the number of dimensions specified in the NIFTI header, as trailing dimensions of length 1 are ignored. But it is guaranteed to be @@ -500,6 +500,15 @@ class Nifti(notifier.Notifier, meta.Meta): return len(self.__shape) + @property + @deprecation.deprecated(deprecated_in='1.9.0', + removed_in='2.0.0', + details='Use ndim instead') + def ndims(self): + """Deprecated - use :mod::meth:``ndim`` instead. """ + return self.ndim() + + @deprecation.deprecated(deprecated_in='1.1.0', removed_in='2.0.0', details='Use ndims instead') @@ -1006,6 +1015,17 @@ class Image(Nifti): return self.__nibImage + @property + def data(self): + """Returns the image data as a ``numpy`` array. + + .. warning:: Calling this method will cause the entire image to be + loaded into memory. + """ + self.__imageWrapper.loadData() + return self[:] + + @property def saveState(self): """Returns ``True`` if this ``Image`` has been saved to disk, ``False`` diff --git a/fsl/data/melodicimage.py b/fsl/data/melodicimage.py index 4d7f05809f5be53ea7f24344df97364a0e97c1d0..ad8922f85dbeb5adf792c9a384db7757615beb5a 100644 --- a/fsl/data/melodicimage.py +++ b/fsl/data/melodicimage.py @@ -77,7 +77,7 @@ class MelodicImage(fslimage.Image): dataImage = fslimage.Image(dataFile, loadData=False, calcRange=False) - if dataImage.ndims >= 4: + if dataImage.ndim >= 4: self.__tr = dataImage.pixdim[3] diff --git a/fsl/utils/platform.py b/fsl/utils/platform.py index 63c1d697c441664fca6bff9f8d96ffc2fec5da4e..cd2764ef9a9accc9a9f0f1a9d4f7e7837a687a8f 100644 --- a/fsl/utils/platform.py +++ b/fsl/utils/platform.py @@ -88,7 +88,6 @@ def isWidgetAlive(widget): import wx - if platform.wxFlavour == platform.WX_PHOENIX: excType = RuntimeError elif platform.wxFlavour == platform.WX_PYTHON: @@ -116,6 +115,7 @@ class Platform(notifier.Notifier): os frozen fsldir + fsldevdir haveGui canHaveGui inSSHSession @@ -146,8 +146,9 @@ class Platform(notifier.Notifier): self.__glRenderer = None self.__glIsSoftware = None self.__fslVersion = None - self.__fsldir = None - self.fsldir = os.environ.get('FSLDIR', None) + + # initialise fsldir - see fsldir.setter + self.fsldir = self.fsldir # Determine if a display is available. We do # this once at init (instead of on-demand in @@ -284,7 +285,13 @@ class Platform(notifier.Notifier): any registered listeners are notified via the :class:`.Notifier` interface. """ - return self.__fsldir + return os.environ.get('FSLDIR', None) + + + @property + def fsldevdir(self): + """The FSL development directory location. """ + return os.environ.get('FSLDEVDIR', None) @fsldir.setter @@ -301,9 +308,9 @@ class Platform(notifier.Notifier): elif not op.exists(value): value = None elif not op.isdir(value): value = None - self.__fsldir = value - - if value is not None: + if value is None: + os.environ.pop('FSLDIR', None) + else: os.environ['FSLDIR'] = value # Set the FSL version field if we can @@ -316,6 +323,26 @@ class Platform(notifier.Notifier): self.notify(value=value) + @fsldevdir.setter + def fsldevdir(self, value): + """Changes the value of the :attr:`fsldevdir` property, and notifies + any registered listeners. + """ + + if value is not None: + value = value.strip() + + if value is None: pass + elif value == '': value = None + elif not op.exists(value): value = None + elif not op.isdir(value): value = None + + if value is None: + os.environ.pop('FSLDEVDIR', None) + else: + os.environ['FSLDEVDIR'] = value + + @property def fslVersion(self): """Returns the FSL version as a string, e.g. ``'5.0.9'``. Returns diff --git a/fsl/utils/run.py b/fsl/utils/run.py index 032e07d97a91951e61026b5e49f757675b799bd9..a8adb80b879a198485970284d562ef6064c96d96 100644 --- a/fsl/utils/run.py +++ b/fsl/utils/run.py @@ -16,7 +16,10 @@ """ +import sys import logging +import warnings +import threading import contextlib import collections import subprocess as sp @@ -26,6 +29,7 @@ import six from fsl.utils.platform import platform as fslplatform import fsl.utils.fslsub as fslsub +import fsl.utils.tempdir as tempdir log = logging.getLogger(__name__) @@ -37,6 +41,10 @@ execute them. """ +FSL_PREFIX = None +"""Global override for the FSL executable location used by :func:`runfsl`. """ + + class FSLNotPresent(Exception): """Error raised by the :func:`runfsl` function when ``$FSLDIR`` cannot be found. @@ -80,6 +88,36 @@ def _prepareArgs(args): return list(args) +real_stdout = sys.stdout +def _forwardStream(in_, *outs): + """Creates and starts a daemon thread which forwards the given input stream + to one or more output streams. Used by the :func:`run` function to redirect + a command's standard output/error streams to more than one destination. + + It is necessary to read the process stdout/ stderr on separate threads to + avoid deadlocks. + + :arg in_: Input stream + :arg outs: Output stream(s) + :returns: The thread that has been started. + """ + + # not all file-likes have a mode attribute - + # if not present, assume a string stream + omodes = [getattr(o, 'mode', 'w') for o in outs] + + def realForward(): + for line in in_: + for i, o in enumerate(outs): + if 'b' in omodes[i]: o.write(line) + else: o.write(line.decode('utf-8')) + + t = threading.Thread(target=realForward) + t.daemon = True + t.start() + return t + + def run(*args, **kwargs): """Call a command and return its output. You can pass the command and arguments as a single string, or as a regular or unpacked sequence. @@ -90,39 +128,79 @@ def run(*args, **kwargs): An exception is raised if the command returns a non-zero exit code, unless the ``ret`` option is set to ``True``. - :arg submit: Must be passed as a keyword argument. Defaults to ``None``. - Accepted values are ``True`` or a - If ``True``, the command is submitted as a cluster job via - the :func:`.fslsub.submit` function. May also be a - dictionary containing arguments to that function. - - :arg err: Must be passed as a keyword argument. Defaults to - ``False``. If ``True``, standard error is captured and - returned. Ignored if ``submit`` is specified. - - :arg ret: Must be passed as a keyword argument. Defaults to ``False``. - If ``True``, and the command's return code is non-0, an - exception is not raised. Ignored if ``submit`` is specified. - - :returns: If ``submit`` is provided, the cluster job ID is returned. - Otherwise if ``err is False and ret is False`` (the default) - a string containing the command's standard output. is - returned. Or, if ``err is True`` and/or ``ret is True``, a - tuple containing the standard output, standard error (if - ``err``), and return code (if ``ret``). + :arg stdout: Must be passed as a keyword argument. Defaults to ``True``. + If ``True``, standard output is captured and returned. + Ignored if ``submit`` is specified. + + :arg stderr: Must be passed as a keyword argument. Defaults to ``False``. + If ``True``, standard error is captured and returned. + Ignored if ``submit`` is specified. + + :arg exitcode: Must be passed as a keyword argument. Defaults to ``False``. + If ``True``, and the command's return code is non-0, an + exception is not raised. Ignored if ``submit`` is + specified. + + :arg err: Deprecated - use ``stderr`` instead. + + :arg ret: Deprecated - use ``exitcode`` instead. + + :arg submit: Must be passed as a keyword argument. Defaults to ``None``. + If ``True``, the command is submitted as a cluster job via + the :func:`.fslsub.submit` function. May also be a + dictionary containing arguments to that function. + + :arg log: Must be passed as a keyword argument. An optional ``dict`` + which may be used to redirect the command's standard output + and error. The following keys are recognised: + + - tee: If ``True``, the command's standard output/error + streams are forwarded to this processes streams. + + - stdout: Optional file-like object to which the command's + standard output stream can be forwarded. + + - stderr: Optional file-like object to which the command's + standard error stream can be forwarded. + + - cmd: If ``True``, the command itself is logged to the + standard output stream(s). + + :returns: If ``submit`` is provided, the return value of + :func:`.fslsub` is returned. Otherwise returns a single + value or a tuple, based on the based on the ``stdout``, + ``stderr``, and ``exitcode`` arguments. """ - err = kwargs.get('err', False) - ret = kwargs.get('ret', False) - submit = kwargs.get('submit', None) - args = _prepareArgs(args) + if 'err' in kwargs: + warnings.warn('err is deprecated and will be removed ' + 'in fslpy 2.0.0 - use stderr instead', + DeprecationWarning) + kwargs['stderr'] = kwargs.get('stderr', kwargs['err']) + if 'ret' in kwargs: + warnings.warn('ret is deprecated and will be removed ' + 'in fslpy 2.0.0 - use exitcode instead', + DeprecationWarning) + kwargs['exitcode'] = kwargs.get('exitcode', kwargs['ret']) + + returnStdout = kwargs.get('stdout', True) + returnStderr = kwargs.get('stderr', False) + returnExitcode = kwargs.get('exitcode', False) + submit = kwargs.get('submit', {}) + log = kwargs.get('log', {}) + tee = log .get('tee', False) + logStdout = log .get('stdout', None) + logStderr = log .get('stderr', None) + logCmd = log .get('cmd', False) + args = _prepareArgs(args) if not bool(submit): submit = None if submit is not None: - err = False - ret = False + returnStdout = False + returnStderr = False + returnExitcode = False if submit is True: submit = dict() @@ -132,54 +210,150 @@ def run(*args, **kwargs): 'options for fsl.utils.fslsub.submit') if DRY_RUN: - log.debug('dryrun: {}'.format(' '.join(args))) - else: - log.debug('run: {}'.format(' '.join(args))) + return _dryrun( + submit, returnStdout, returnStderr, returnExitcode, *args) - if DRY_RUN: - stderr = '' - if submit is None: - stdout = ' '.join(args) - else: - stdout = '[submit] ' + ' '.join(args) - - elif submit is not None: + # submit - delegate to fslsub + if submit is not None: return fslsub.submit(' '.join(args), **submit) - else: - proc = sp.Popen(args, stdout=sp.PIPE, stderr=sp.PIPE) - stdout, stderr = proc.communicate() - retcode = proc.returncode + # Run directly - delegate to _realrun + stdout, stderr, exitcode = _realrun( + tee, logStdout, logStderr, logCmd, *args) + + if not returnExitcode and (exitcode != 0): + raise RuntimeError('{} returned non-zero exit code: {}'.format( + args[0], exitcode)) - stdout = stdout.decode('utf-8').strip() - stderr = stderr.decode('utf-8').strip() + results = [] + if returnStdout: results.append(stdout) + if returnStderr: results.append(stderr) + if returnExitcode: results.append(exitcode) - log.debug('stdout: {}'.format(stdout)) - log.debug('stderr: {}'.format(stderr)) + if len(results) == 1: return results[0] + else: return tuple(results) + + + +def _dryrun(submit, returnStdout, returnStderr, returnExitcode, *args): + """Used by the :func:`run` function when the :attr:`DRY_RUN` flag is + active. + """ - if not ret and (retcode != 0): - raise RuntimeError('{} returned non-zero exit code: {}'.format( - args[0], retcode)) + if submit: + return ('0',) - results = [stdout] + results = [] + stderr = '' + stdout = ' '.join(args) - if err: results.append(stderr) - if ret: results.append(retcode) + if returnStdout: results.append(stdout) + if returnStderr: results.append(stderr) + if returnExitcode: results.append(0) if len(results) == 1: return results[0] else: return tuple(results) +def _realrun(tee, logStdout, logStderr, logCmd, *args): + """Used by :func:`run`. Runs the given command and manages its standard + output and error streams. + + :arg tee: If ``True``, the command's standard output and error + streams are forwarded to this process' standard output/ + error. + + :arg logStdout: Optional file-like object to which the command's standard + output stream can be forwarded. + + :arg logStderr: Optional file-like object to which the command's standard + error stream can be forwarded. + + :arg logCmd: If ``True``, the command itself is logged to the standard + output stream(s). + + :arg args: Command to run + + :returns: A tuple containing: + - the command's standard output as a string. + - the command's standard error as a string. + - the command's exit code. + """ + proc = sp.Popen(args, stdout=sp.PIPE, stderr=sp.PIPE) + with tempdir.tempdir(changeto=False) as td: + + # We always direct the command's stdout/ + # stderr to two temporary files + stdoutf = op.join(td, 'stdout') + stderrf = op.join(td, 'stderr') + + with open(stdoutf, 'wb') as stdout, \ + open(stderrf, 'wb') as stderr: # noqa + + outstreams = [stdout] + errstreams = [stderr] + + # if tee, we duplicate the command's + # stdout/stderr to this process' + # stdout/stderr + if tee: + outstreams.append(sys.stdout) + errstreams.append(sys.stderr) + + # And we also duplicate to caller- + # provided streams if they're given. + if logStdout is not None: outstreams.append(logStdout) + if logStderr is not None: errstreams.append(logStderr) + + # log the command to + # stdout if requested + if logCmd: + cmd = ' '.join(args) + '\n' + for o in outstreams: + if 'b' in getattr(o, 'mode', 'w'): + o.write(cmd.encode('utf-8')) + else: + o.write(cmd) + + stdoutt = _forwardStream(proc.stdout, *outstreams) + stderrt = _forwardStream(proc.stderr, *errstreams) + + # Wait until the forwarding threads + # have finished cleanly, and the + # command has terminated. + stdoutt.join() + stderrt.join() + proc.communicate() + + # Read in the command's stdout/stderr + with open(stdoutf, 'rb') as f: stdout = f.read() + with open(stderrf, 'rb') as f: stderr = f.read() + + exitcode = proc.returncode + stdout = stdout.decode('utf-8') + stderr = stderr.decode('utf-8') + + return stdout, stderr, exitcode + + def runfsl(*args, **kwargs): """Call a FSL command and return its output. This function simply prepends ``$FSLDIR/bin/`` to the command before passing it to :func:`run`. """ - if fslplatform.fsldir is None: + prefix = None + + if FSL_PREFIX is not None: + prefix = FSL_PREFIX + elif fslplatform.fsldevdir is not None: + prefix = op.join(fslplatform.fsldevdir, 'bin') + elif fslplatform.fsldir is not None: + prefix = op.join(fslplatform.fsldir, 'bin') + else: raise FSLNotPresent('$FSLDIR is not set - FSL cannot be found!') args = _prepareArgs(args) - args[0] = op.join(fslplatform.fsldir, 'bin', args[0]) + args[0] = op.join(prefix, args[0]) return run(*args, **kwargs) diff --git a/fsl/wrappers/bet.py b/fsl/wrappers/bet.py index 45e7870de684fb7a0d23c15a2994000b37869524..969dcb3cdf7920f20d8dcb15fd05e6bd6adf03eb 100644 --- a/fsl/wrappers/bet.py +++ b/fsl/wrappers/bet.py @@ -39,8 +39,19 @@ def bet(input, output, **kwargs): valmap = { 'm' : wutils.SHOW_IF_TRUE, - 'R' : wutils.SHOW_IF_TRUE, + 'o' : wutils.SHOW_IF_TRUE, + 's' : wutils.SHOW_IF_TRUE, 'n' : wutils.HIDE_IF_TRUE, + 't' : wutils.SHOW_IF_TRUE, + 'e' : wutils.SHOW_IF_TRUE, + 'R' : wutils.SHOW_IF_TRUE, + 'S' : wutils.SHOW_IF_TRUE, + 'B' : wutils.SHOW_IF_TRUE, + 'Z' : wutils.SHOW_IF_TRUE, + 'F' : wutils.SHOW_IF_TRUE, + 'A' : wutils.SHOW_IF_TRUE, + 'v' : wutils.SHOW_IF_TRUE, + 'd' : wutils.SHOW_IF_TRUE, } cmd = ['bet', input, output] diff --git a/fsl/wrappers/wrapperutils.py b/fsl/wrappers/wrapperutils.py index a1bc938d96ba57ddd95956a45c139fc348f3cb18..cfeed50ffa9b6dcdeb87c32075c22202772c77b9 100644 --- a/fsl/wrappers/wrapperutils.py +++ b/fsl/wrappers/wrapperutils.py @@ -142,8 +142,10 @@ def cmdwrapper(func): """ def wrapper(*args, **kwargs): submit = kwargs.pop('submit', None) - cmd = func(*args, **kwargs) - return run.run(cmd, err=True, submit=submit) + stderr = kwargs.pop('stderr', True) + log = kwargs.pop('log', {'tee' : True}) + cmd = func(*args, **kwargs) + return run.run(cmd, stderr=stderr, log=log, submit=submit) return _update_wrapper(wrapper, func) @@ -154,8 +156,10 @@ def fslwrapper(func): """ def wrapper(*args, **kwargs): submit = kwargs.pop('submit', None) - cmd = func(*args, **kwargs) - return run.runfsl(cmd, err=True, submit=submit) + stderr = kwargs.pop('stderr', True) + log = kwargs.pop('log', {'tee' : True}) + cmd = func(*args, **kwargs) + return run.runfsl(cmd, stderr=stderr, log=log, submit=submit) return _update_wrapper(wrapper, func) diff --git a/tests/__init__.py b/tests/__init__.py index 57794123f0bbb3aa3ee448f742039781015cbb49..d8647089277169add9aead66a38bd911615448e3 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -36,7 +36,8 @@ logging.getLogger().setLevel(logging.WARNING) @contextlib.contextmanager def mockFSLDIR(): - oldval = fslplatform.fsldir + oldfsldir = fslplatform.fsldir + oldfsldevdir = fslplatform.fsldevdir try: with tempdir() as td: @@ -44,13 +45,15 @@ def mockFSLDIR(): bindir = op.join(fsldir, 'bin') os.makedirs(bindir) fslplatform.fsldir = fsldir + fslplatform.fsldevdir = None path = op.pathsep.join((bindir, os.environ['PATH'])) with mock.patch.dict(os.environ, {'PATH': path}): yield fsldir finally: - fslplatform.fsldir = oldval + fslplatform.fsldir = oldfsldir + fslplatform.fsldevdir = oldfsldevdir def touch(fname): @@ -67,6 +70,9 @@ class CaptureStdout(object): def reset(self): self.__mock_stdout = StringIO('') self.__mock_stderr = StringIO('') + self.__mock_stdout.mode = 'w' + self.__mock_stderr.mode = 'w' + return self def __enter__(self): self.__real_stdout = sys.stdout diff --git a/tests/test_image.py b/tests/test_image.py index 841e1620ffde74b403131758e271de393ad94177..74e4a9a72575d243abe666b27aa43bea9f4b07e6 100644 --- a/tests/test_image.py +++ b/tests/test_image.py @@ -285,11 +285,12 @@ def _test_Image_atts(imgtype): i = fslimage.Image(path) assert tuple(i.shape) == tuple(expdims) + assert tuple(i.data.shape) == tuple(expdims) assert tuple(i.pixdim) == tuple(exppixdims) assert tuple(i.nibImage.shape) == tuple(dims) assert tuple(i.nibImage.header.get_zooms()) == tuple(pixdims) - assert i.ndims == expndims + assert i.ndim == expndims assert i.dtype == dtype assert i.name == op.basename(path) assert i.dataSource == fslpath.addExt(path, @@ -859,8 +860,9 @@ def _test_Image_5D(imgtype): img = fslimage.Image(path) - assert img.shape == dims - assert img.ndims == 5 + assert img.shape == dims + assert img.ndim == 5 + assert img.data.shape == dims def test_Image_voxToScaledVox_analyze(): _test_Image_voxToScaledVox(0) diff --git a/tests/test_run.py b/tests/test_run.py index 4b482fb090f9bc46753c0f13f504b234b3a5d709..9a260de5c543b71132b42b6ca716633b8dfbda1f 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -8,6 +8,7 @@ import os.path as op import os +import shutil import textwrap # python 3 @@ -23,7 +24,13 @@ from fsl.utils.platform import platform as fslplatform import fsl.utils.run as run import fsl.utils.fslsub as fslsub -from . import make_random_image, mockFSLDIR +from . import make_random_image, mockFSLDIR, CaptureStdout + + +def mkexec(path, contents): + with open(path, 'wt') as f: + f.write(contents) + os.chmod(path, 0o755) def test_run(): @@ -39,50 +46,106 @@ def test_run(): with tempdir.tempdir(): # return code == 0 - with open('script.sh', 'wt') as f: - f.write(test_script.format(0)) - os.chmod('script.sh', 0o755) + mkexec('script.sh', test_script.format(0)) - expstdout = "standard output - arguments: 1 2 3" - expstderr = "standard error" + expstdout = "standard output - arguments: 1 2 3\n" + expstderr = "standard error\n" # test: # - single string # - packed sequence # - unpacked sequence - assert run.run('./script.sh 1 2 3').strip() == expstdout - assert run.run(('./script.sh', '1', '2', '3')) == expstdout + assert run.run('./script.sh 1 2 3') == expstdout + assert run.run(('./script.sh', '1', '2', '3')) == expstdout assert run.run(*('./script.sh', '1', '2', '3')) == expstdout # test stdout/stderr - stdout, stderr = run.run('./script.sh 1 2 3', err=True) - assert stdout.strip() == expstdout - assert stderr.strip() == expstderr + stdout, stderr = run.run('./script.sh 1 2 3', stderr=True) + assert stdout == expstdout + assert stderr == expstderr # test return code - res = run.run('./script.sh 1 2 3', ret=True) - print(res) + res = run.run('./script.sh 1 2 3', exitcode=True) stdout, ret = res - assert stdout.strip() == expstdout + assert stdout == expstdout assert ret == 0 - stdout, stderr, ret = run.run('./script.sh 1 2 3', err=True, ret=True) - assert stdout.strip() == expstdout - assert stderr.strip() == expstderr + stdout, stderr, ret = run.run('./script.sh 1 2 3', stderr=True, + exitcode=True) + assert stdout == expstdout + assert stderr == expstderr assert ret == 0 + # stdout=False + res = run.run('./script.sh 1 2 3', stdout=False) + assert res == () + stderr = run.run('./script.sh 1 2 3', stdout=False, stderr=True) + assert stderr == expstderr + # return code != 0 - with open('./script.sh', 'wt') as f: - f.write(test_script.format(255)) - os.chmod('./script.sh', 0o755) + mkexec('./script.sh', test_script.format(255)) with pytest.raises(RuntimeError): run.run('./script.sh 1 2 3') - stdout, ret = run.run('./script.sh 1 2 3', ret=True) - assert stdout.strip() == expstdout + stdout, ret = run.run('./script.sh 1 2 3', exitcode=True) + assert stdout == expstdout assert ret == 255 +def test_run_tee(): + test_script = textwrap.dedent(""" + #!/bin/bash + + echo "standard output - arguments: $@" + echo "standard error" >&2 + exit 0 + """).strip() + + with tempdir.tempdir(): + mkexec('script.sh', test_script) + + expstdout = "standard output - arguments: 1 2 3\n" + expstderr = "standard error\n" + + capture = CaptureStdout() + + with capture: + stdout = run.run('./script.sh 1 2 3', log={'tee' : True}) + + assert stdout == expstdout + assert capture.stdout == expstdout + + with capture.reset(): + stdout, stderr = run.run('./script.sh 1 2 3', stderr=True, + log={'tee' : True}) + + assert stdout == expstdout + assert stderr == expstderr + assert capture.stdout == expstdout + assert capture.stderr == expstderr + + with capture.reset(): + stdout, stderr, ret = run.run('./script.sh 1 2 3', + stderr=True, + exitcode=True, + log={'tee' : True}) + + assert ret == 0 + assert stdout == expstdout + assert stderr == expstderr + assert capture.stdout == expstdout + assert capture.stderr == expstderr + + with capture.reset(): + stdout, ret = run.run('./script.sh 1 2 3', + exitcode=True, + log={'tee' : True}) + + assert ret == 0 + assert stdout == expstdout + assert capture.stdout == expstdout + + def test_dryrun(): test_script = textwrap.dedent(""" @@ -91,9 +154,7 @@ def test_dryrun(): """).strip() with tempdir.tempdir(): - with open('./script.sh', 'wt') as f: - f.write(test_script) - os.chmod('./script.sh', 0o755) + mkexec('./script.sh', test_script) run.run('./script.sh') assert op.exists('foo') @@ -111,11 +172,12 @@ def test_runfsl(): test_script = textwrap.dedent(""" #!/bin/bash - echo $@ + echo {} exit 0 """).strip() - old_fsldir = fslplatform.fsldir + old_fsldir = fslplatform.fsldir + old_fsldevdir = fslplatform.fsldevdir try: with tempdir.tempdir(): @@ -123,24 +185,47 @@ def test_runfsl(): make_random_image('image.nii.gz') # no FSLDIR - should error - fslplatform.fsldir = None + fslplatform.fsldir = None + fslplatform.fsldevdir = None with pytest.raises(run.FSLNotPresent): - run.runfsl('fslhd image') + run.runfsl('fslhd') # FSLDIR/bin exists - should be good fsldir = op.abspath('./fsl') fslhd = op.join(fsldir, 'bin', 'fslhd') os.makedirs(op.join(fsldir, 'bin')) - with open(fslhd, 'wt') as f: - f.write(test_script) - os.chmod(fslhd, 0o777) + + mkexec(fslhd, test_script.format('fsldir')) fslplatform.fsldir = fsldir - path = op.pathsep.join((fsldir, os.environ['PATH'])) - with mock.patch.dict(os.environ, {'PATH' : path}): - assert run.runfsl('fslhd image').strip() == 'image' + assert run.runfsl('fslhd').strip() == 'fsldir' + + # FSLDEVDIR should take precedence + fsldevdir = './fsldev' + fslhd = op.join(fsldevdir, 'bin', 'fslhd') + shutil.copytree(fsldir, fsldevdir) + + mkexec(fslhd, test_script.format('fsldevdir')) + + fslplatform.fsldevdir = fsldevdir + fslplatform.fsldir = None + assert run.runfsl('fslhd').strip() == 'fsldevdir' + + # FSL_PREFIX should override all + override = './override' + fslhd = op.join(override, 'fslhd') + os.makedirs(override) + mkexec(fslhd, test_script.format('override')) + + fslplatform.fsldir = None + fslplatform.fsldevdir = None + run.FSL_PREFIX = override + assert run.runfsl('fslhd').strip() == 'override' + finally: - fslplatform.fsldir = old_fsldir + fslplatform.fsldir = old_fsldir + fslplatform.fsldevdir = old_fsldevdir + run.FSL_PREFIX = None def mock_submit(cmd, **kwargs): @@ -189,8 +274,8 @@ def test_run_submit(): stdout, stderr = fslsub.output(jid) - assert stdout.strip() == 'test_script running' - assert stderr.strip() == '' + assert stdout == 'test_script running\n' + assert stderr == '' kwargs = {'name' : 'abcde', 'ram' : '4GB'} @@ -201,7 +286,84 @@ def test_run_submit(): stdout, stderr = fslsub.output(jid) experr = '\n'.join(['{}: {}'.format(k, kwargs[k]) - for k in sorted(kwargs.keys())]) + for k in sorted(kwargs.keys())]) + '\n' + + assert stdout == 'test_script running\n' + assert stderr == experr + + +def test_run_streams(): + """ + """ + + test_script = textwrap.dedent(""" + #!/usr/bin/env bash + echo standard output + echo standard error >&2 + exit 0 + """).strip() + + expstdout = 'standard output\n' + expstderr = 'standard error\n' + + with tempdir.tempdir(): + mkexec('./script.sh', test_script) + + with open('my_stdout', 'wt') as stdout, \ + open('my_stderr', 'wt') as stderr: + + stdout, stderr = run.run('./script.sh', + stderr=True, + log={'stdout' : stdout, + 'stderr' : stderr}) + + assert stdout == expstdout + assert stderr == expstderr + assert open('my_stdout', 'rt').read() == expstdout + assert open('my_stderr', 'rt').read() == expstderr + + + capture = CaptureStdout() + + with open('my_stdout', 'wt') as stdout, \ + open('my_stderr', 'wt') as stderr, \ + capture.reset(): + + stdout, stderr = run.run('./script.sh', + stderr=True, + log={'tee' : True, + 'stdout' : stdout, + 'stderr' : stderr}) + + assert stdout == expstdout + assert stderr == expstderr + assert capture.stdout == expstdout + assert capture.stderr == expstderr + assert open('my_stdout', 'rt').read() == expstdout + assert open('my_stderr', 'rt').read() == expstderr + + +def test_run_logcmd(): + test_script = textwrap.dedent(""" + #!/usr/bin/env bash + echo output $@ + exit 0 + """).strip() + + expstdout = './script.sh 1 2 3\noutput 1 2 3\n' + + with tempdir.tempdir(): + mkexec('script.sh', test_script) + stdout = run.run('./script.sh 1 2 3', log={'cmd' : True}) + assert stdout == expstdout + + mkexec('script.sh', test_script) + stdout = run.run('./script.sh 1 2 3', log={'cmd' : True}) + assert stdout == expstdout + + with open('my_stdout', 'wt') as stdoutf: + stdout = run.run('./script.sh 1 2 3', + log={'cmd' : True, 'stdout' : stdoutf}) - assert stdout.strip() == 'test_script running' - assert stderr.strip() == experr + assert stdout == expstdout + assert open('my_stdout', 'rt').read() == expstdout