diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7f46e780467d7fd2a594e3b50ed942c316807919..96f59e61235dd6bc3b8a775e1a3ca01b2c19cd2e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,25 @@ This document contains the ``fslpy`` release history in reverse chronological order. +3.3.0 (Under development) +------------------------- + + +Added +^^^^^ + +* New ported versions of various core FSL tools, including ``imrm``, ``imln``, + ``imtest``, ``fsl_abspath``, ``remove_ext``, ``Text2Vest``, and + ``Vest2Text``. + + +Changed +^^^^^^^ + + +* Adjustments to tests and documentation + + 3.2.2 (Thursday 9th July 2020) ------------------------------ diff --git a/fsl/data/image.py b/fsl/data/image.py index ce01645b4c544f263c0ea05cd2cd38be7fa0f604..7c2bdbcba722e8f0017ce0bba46269283f8e4d8b 100644 --- a/fsl/data/image.py +++ b/fsl/data/image.py @@ -1625,18 +1625,20 @@ def removeExt(filename): return fslpath.removeExt(filename, ALLOWED_EXTENSIONS) -def fixExt(filename): +def fixExt(filename, **kwargs): """Fix the extension of ``filename``. For example, if a file name is passed in as ``file.nii.gz``, but the file is actually ``file.nii``, this function will fix the file name. If ``filename`` already exists, it is returned unchanged. + + All other arguments are passed through to :func:`addExt`. """ if op.exists(filename): return filename else: - return addExt(removeExt(filename)) + return addExt(removeExt(filename), **kwargs) def defaultExt(): diff --git a/fsl/data/vest.py b/fsl/data/vest.py index b0b33c73fa41ab045c849d079f52d73d40baccf6..9fe10c103ad8dc278d4ec4b163883803ecc6830f 100644 --- a/fsl/data/vest.py +++ b/fsl/data/vest.py @@ -11,9 +11,14 @@ looksLikeVestLutFile loadVestLutFile + loadVestFile + generateVest """ +import textwrap as tw +import io + import numpy as np @@ -76,3 +81,70 @@ def loadVestLutFile(path, normalise=True): else: return colours + + +def loadVestFile(path, ignoreHeader=True): + """Loads numeric data from a VEST file, returning it as a ``numpy`` array. + + :arg ignoreHeader: if ``True`` (the default), the matrix shape specified + in the VEST header information is ignored, and the shape + inferred from the data. Otherwise, if the number of + rows/columns specified in the VEST header information + does not match the matrix shape, a ``ValueError`` is + raised. + + :returns: a ``numpy`` array containing the matrix data in the + VEST file. + """ + + data = np.loadtxt(path, comments=['#', '/']) + + if not ignoreHeader: + nrows, ncols = None, None + with open(path, 'rt') as f: + for line in f: + if 'NumWaves' in line: ncols = int(line.split()[1]) + elif 'NumPoints' in line: nrows = int(line.split()[1]) + else: continue + + if (ncols is not None) and (nrows is not None): + break + + if tuple(data.shape) != (nrows, ncols): + raise ValueError(f'Invalid VEST file ({path}) - data shape ' + f'({data.shape}) does not match header ' + f'({nrows}, {ncols})') + + return data + + +def generateVest(data): + """Generates VEST-formatted text for the given ``numpy`` array. + + :arg data: A 1D or 2D numpy array. + :returns: A string containing a VEST header, and the ``data``. + """ + + data = np.asanyarray(data) + + if len(data.shape) not in (1, 2): + raise ValueError(f'unsupported number of dimensions: {data.shape}') + + data = np.atleast_2d(data) + + if np.issubdtype(data.dtype, np.integer): fmt = '%d' + else: fmt = '%0.12f' + + sdata = io.StringIO() + np.savetxt(sdata, data, fmt=fmt) + sdata = sdata.getvalue() + + nrows, ncols = data.shape + + vest = tw.dedent(f""" + /NumWaves {ncols} + /NumPoints {nrows} + /Matrix + """).strip() + '\n' + sdata + + return vest.strip() diff --git a/fsl/scripts/Text2Vest.py b/fsl/scripts/Text2Vest.py new file mode 100644 index 0000000000000000000000000000000000000000..15989f945f0a39ef73163e9c6840eff893f2e5c7 --- /dev/null +++ b/fsl/scripts/Text2Vest.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# +# Text2Vest.py - Convert an ASCII text matrix file into a VEST file. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# +"""``Text2Vest`` simply takes a plain text ASCII text matrix file, and +adds a VEST header. +""" + + +import sys + +import numpy as np + +import fsl.data.vest as fslvest + + +usage = "Usage: Text2Vest <text_file> <vest_file>" + + +def main(argv=None): + """Convert a plain text file to a VEST file. """ + + if argv is None: + argv = sys.argv[1:] + + if len(argv) != 2: + print(usage) + return 0 + + infile, outfile = argv + data = np.loadtxt(infile) + vest = fslvest.generateVest(data) + + with open(outfile, 'wt') as f: + f.write(vest) + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/fsl/scripts/Vest2Text.py b/fsl/scripts/Vest2Text.py new file mode 100644 index 0000000000000000000000000000000000000000..b12d8166a32a8ffd8eb2de33ada2e886ba9f8b39 --- /dev/null +++ b/fsl/scripts/Vest2Text.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# +# Vest2Text.py - Convert a VEST matrix file into a plain text ASCII file. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# +"""``Vest2Text`` takes a VEST file containing a 2D matrix, and converts it +into a plain-text ASCII file. +""" + + +import sys + +import numpy as np + +import fsl.data.vest as fslvest + + +usage = "Usage: Vest2Text <vest_file> <text_file>" + + +def main(argv=None): + """Convert a VEST file to a plain text file. """ + + if argv is None: + argv = sys.argv[1:] + + if len(argv) != 2: + print(usage) + return 0 + + infile, outfile = argv + data = fslvest.loadVestFile(infile) + + if np.issubdtype(data.dtype, np.integer): fmt = '%d' + else: fmt = '%0.12f' + + np.savetxt(outfile, data, fmt=fmt) + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/fsl/scripts/__init__.py b/fsl/scripts/__init__.py index 4ef1e04becbabc6280ce9106f84d943d0e8e923b..499b221767c7e66e8ff8bac70c7cc1ff14cf5120 100644 --- a/fsl/scripts/__init__.py +++ b/fsl/scripts/__init__.py @@ -5,5 +5,12 @@ # Author: Paul McCarthy <pauldmccarthy@gmail.com> # """The ``fsl.scripts`` package contains all of the executable scripts provided -by ``fslpy``. +by ``fslpy``, and other python-based FSL packages. + +.. note:: The ``fsl.scripts`` namespace is a ``pkgutil``-style *namespace + package* - it can be used across different projects - see + https://packaging.python.org/guides/packaging-namespace-packages/ for + details. """ + +__path__ = __import__('pkgutil').extend_path(__path__, __name__) # noqa diff --git a/fsl/scripts/fsl_abspath.py b/fsl/scripts/fsl_abspath.py new file mode 100644 index 0000000000000000000000000000000000000000..ed4775874326abda6c6d3881892d6988a006f2a4 --- /dev/null +++ b/fsl/scripts/fsl_abspath.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# +# fsl_abspath.py - Make a path absolute +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# +"""The fsl_abspath command - makes relative paths absolute. +""" + + +import os.path as op +import sys + + +usage = """ +usage: fsl_abspath path +""".strip() + + +def main(argv=None): + """fsl_abspath - make a relative path absolute. """ + + if argv is None: + argv = sys.argv[1:] + + if len(argv) != 1: + print(usage) + return 1 + + print(op.realpath(argv[0])) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/fsl/scripts/fsl_ents.py b/fsl/scripts/fsl_ents.py index eb78ed83ebb585787d5864c67aac98b7b3786296..1590c9b4491865c7256a3ccb87a27c047b88b772 100644 --- a/fsl/scripts/fsl_ents.py +++ b/fsl/scripts/fsl_ents.py @@ -9,8 +9,6 @@ time series from a MELODIC ``.ica`` directory. """ -from __future__ import print_function - import os.path as op import sys import argparse diff --git a/fsl/scripts/imcp.py b/fsl/scripts/imcp.py index 81f53a83d5fdd682c80f41a208641c3273a8985d..d2fcc1eda66a6c01f43bbdb246ce46ade699e82f 100755 --- a/fsl/scripts/imcp.py +++ b/fsl/scripts/imcp.py @@ -12,8 +12,6 @@ The :func:`main` function is essentially a wrapper around the """ -from __future__ import print_function - import os.path as op import sys import warnings diff --git a/fsl/scripts/imglob.py b/fsl/scripts/imglob.py index ed85e4120d2e9024f2848e9be28eab43ce18e068..1e756dee6f79d5e3f4a5f087ba355c5a795771bd 100644 --- a/fsl/scripts/imglob.py +++ b/fsl/scripts/imglob.py @@ -9,8 +9,6 @@ NIFTI/ANALYZE image files. """ -from __future__ import print_function - import sys import warnings import fsl.utils.path as fslpath diff --git a/fsl/scripts/imln.py b/fsl/scripts/imln.py new file mode 100644 index 0000000000000000000000000000000000000000..88955f3cb36300853f58f276f6e4dedb6ffda18e --- /dev/null +++ b/fsl/scripts/imln.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# +# imln.py - Create symbolic links to image files. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# +"""This module defines the ``imln`` application, for creating sym-links +to NIFTI image files. + +.. note:: When creating links to relative paths, ln requires that the path is + relative to the link location, rather than the invocation + location. This is *not* currently supported by imln, and possibly + never will be. +""" + + +import os.path as op +import os +import sys +import warnings + +import fsl.utils.path as fslpath + + +# See atlasq.py for explanation +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=FutureWarning) + import fsl.data.image as fslimage + + +ALLOWED_EXTENSIONS = fslimage.ALLOWED_EXTENSIONS + ['.mnc', '.mnc.gz'] +"""List of file extensions that are supported by ``imln``. """ + + +usage = """ +Usage: imln <file1> <file2> + Makes a link (called file2) to file1 + NB: filenames can be basenames or include an extension +""".strip() + + +def main(argv=None): + """``imln`` - create sym-links to images. """ + + if argv is None: + argv = sys.argv[1:] + + if len(argv) != 2: + print(usage) + return 1 + + target, linkbase = argv + target = fslpath.removeExt(target, ALLOWED_EXTENSIONS) + linkbase = fslpath.removeExt(linkbase, ALLOWED_EXTENSIONS) + + # Target must exist, so we can + # infer the correct extension(s). + # Error on incomplete file groups + # (e.g. a.img without a.hdr). + try: + targets = fslpath.getFileGroup(target, + allowedExts=ALLOWED_EXTENSIONS, + fileGroups=fslimage.FILE_GROUPS, + unambiguous=True) + except Exception as e: + print(f'Error: {e}') + return 1 + + for target in targets: + if not op.exists(target): + continue + + ext = fslpath.getExt(target, ALLOWED_EXTENSIONS) + link = f'{linkbase}{ext}' + + try: + + # emulate old imln behaviour - if + # link already exists, it is removed + if op.exists(link): + os.remove(link) + + os.symlink(target, link) + + except Exception as e: + print(f'Error: {e}') + return 1 + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/fsl/scripts/immv.py b/fsl/scripts/immv.py index c25a99e04c535f3cd26690f5600213e4d3d23ebe..febe422747c27558c98cf6243816980837aaa603 100755 --- a/fsl/scripts/immv.py +++ b/fsl/scripts/immv.py @@ -13,8 +13,6 @@ The :func:`main` function is essentially a wrapper around the """ -from __future__ import print_function - import os.path as op import sys import warnings diff --git a/fsl/scripts/imrm.py b/fsl/scripts/imrm.py new file mode 100644 index 0000000000000000000000000000000000000000..bd81103325b6d8dea25f0b88f02c3bbb40555f6c --- /dev/null +++ b/fsl/scripts/imrm.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# +# imrm.py - Remove image files. +# +# Author: Paul McCarthy <paulmc@fmrib.ox.ac.uk> +# +"""This module defines the ``imrm`` application, for removing NIFTI image +files. +""" + + +import itertools as it +import os.path as op +import os +import sys +import warnings + +import fsl.utils.path as fslpath + +# See atlasq.py for explanation +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=FutureWarning) + import fsl.data.image as fslimage + + +usage = """Usage: imrm <list of image names to remove> +NB: filenames can be basenames or not +""".strip() + + +ALLOWED_EXTENSIONS = fslimage.ALLOWED_EXTENSIONS + ['.mnc', '.mnc.gz'] +"""List of file extensions that are removed by ``imrm``. """ + + +def main(argv=None): + """Removes all images which are specified on the command line. """ + + if argv is None: + argv = sys.argv[1:] + + if len(argv) < 1: + print(usage) + return 1 + + prefixes = [fslpath.removeExt(p, ALLOWED_EXTENSIONS) for p in argv] + + for prefix, ext in it.product(prefixes, ALLOWED_EXTENSIONS): + + path = f'{prefix}{ext}' + + if op.exists(path): + os.remove(path) + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/fsl/scripts/imtest.py b/fsl/scripts/imtest.py new file mode 100644 index 0000000000000000000000000000000000000000..f03c28442b52ed79f535b12aae444e310cf83bc3 --- /dev/null +++ b/fsl/scripts/imtest.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# +# imtest.py - Test whether an image file exists or not. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# +"""The ``imtest`` script can be used to test whether an image file exists or +not, without having to know the file suffix (.nii, .nii.gz, etc). +""" + + +import os.path as op +import sys +import warnings + +import fsl.utils.path as fslpath + +# See atlasq.py for explanation +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=FutureWarning) + import fsl.data.image as fslimage + + +ALLOWED_EXTENSIONS = fslimage.ALLOWED_EXTENSIONS + ['.mnc', '.mnc.gz'] +"""List of file extensions that are supported by ``imln``. """ + + +def main(argv=None): + """Test if an image path exists, and prints ``'1'`` if it does or ``'0'`` + if it doesn't. + """ + + if argv is None: + argv = sys.argv[1:] + + # emulate old fslio/imtest - always return 0 + if len(argv) != 1: + print('0') + return 0 + + path = fslpath.removeExt(argv[0], ALLOWED_EXTENSIONS) + path = op.realpath(path) + + # getFileGroup will raise an error + # if the image (including all + # components - i.e. header and + # image) does not exist + try: + fslpath.getFileGroup(path, + allowedExts=ALLOWED_EXTENSIONS, + fileGroups=fslimage.FILE_GROUPS, + unambiguous=True) + print('1') + except fslpath.PathError: + print('0') + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/fsl/scripts/remove_ext.py b/fsl/scripts/remove_ext.py new file mode 100644 index 0000000000000000000000000000000000000000..fc21094ff113576cc808f25fc3dbde1b7f38ff4f --- /dev/null +++ b/fsl/scripts/remove_ext.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# +# remove_ext.py - Remove file extensions from NIFTI image paths +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + + +import sys +import warnings + +import fsl.utils.path as fslpath + +# See atlasq.py for explanation +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=FutureWarning) + import fsl.data.image as fslimage + + +usage = """Usage: remove_ext <list of image paths to remove extension from> +""".strip() + + +ALLOWED_EXTENSIONS = fslimage.ALLOWED_EXTENSIONS + ['.mnc', '.mnc.gz'] +"""List of file extensions that are removed by ``remove_ext``. """ + + +def main(argv=None): + """Removes file extensions from all paths which are specified on the + command line. + """ + + if argv is None: + argv = sys.argv[1:] + + if len(argv) < 1: + print(usage) + return 1 + + removed = [] + + for path in argv: + removed.append(fslpath.removeExt(path, ALLOWED_EXTENSIONS)) + + print(' '.join(removed)) + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/setup.py b/setup.py index eee141bd0972ba372349a7d1037d076d7b9dd021..1a971bf2a3eb5facc9b5b4ca56168ee8aa9b485d 100644 --- a/setup.py +++ b/setup.py @@ -118,9 +118,16 @@ setup( entry_points={ 'console_scripts' : [ - 'immv = fsl.scripts.immv:main', 'imcp = fsl.scripts.imcp:main', + 'imln = fsl.scripts.imln:main', + 'immv = fsl.scripts.immv:main', + 'imrm = fsl.scripts.imrm:main', 'imglob = fsl.scripts.imglob:main', + 'imtest = fsl.scripts.imtest:main', + 'remove_ext = fsl.scripts.remove_ext:main', + 'fsl_abspath = fsl.scripts.fsl_abspath:main', + 'Text2Vest = fsl.scripts.Text2Vest:main', + 'Vest2Text = fsl.scripts.Vest2Text:main', 'atlasq = fsl.scripts.atlasq:main', 'atlasquery = fsl.scripts.atlasq:atlasquery_emulation', 'fsl_ents = fsl.scripts.fsl_ents:main', diff --git a/tests/test_imglob.py b/tests/test_imglob.py deleted file mode 100644 index 9a8e838d1321fd45c91c1d94188700067423097f..0000000000000000000000000000000000000000 --- a/tests/test_imglob.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python -# -# test_imglob.py - -# -# Author: Paul McCarthy <pauldmccarthy@gmail.com> -# - -import pytest - -import fsl.scripts.imglob as imglob - -from . import testdir - - -def test_imglob_shouldPass(): - - # (files to create, paths, output, expected) - tests = [ - - # normal usage, one file - ('file.hdr file.img', 'file', None, 'file'), - ('file.hdr file.img', 'file', 'prefix', 'file'), - ('file.hdr file.img', 'file', 'primary', 'file.hdr'), - ('file.hdr file.img', 'file', 'all', 'file.hdr file.img'), - - # incomplete file pair - ('file.hdr', 'file', 'prefix', 'file'), - ('file.hdr', 'file.hdr', 'prefix', 'file'), - ('file.hdr', 'file.img', 'prefix', 'file'), - ('file.hdr', 'file', 'primary', 'file.hdr'), - ('file.hdr', 'file.hdr', 'primary', 'file.hdr'), - ('file.hdr', 'file.img', 'primary', 'file.hdr'), - ('file.hdr', 'file', 'all', 'file.hdr'), - ('file.hdr', 'file.hdr', 'all', 'file.hdr'), - ('file.hdr', 'file.img', 'all', 'file.hdr'), - - # same file specified multiple times - ('file.hdr file.img', 'file file', 'prefix', 'file'), - ('file.hdr file.img', 'file file.img', 'prefix', 'file'), - ('file.hdr file.img', 'file file.img file.hdr', 'prefix', 'file'), - ('file.hdr file.img', 'file file', 'primary', 'file.hdr'), - ('file.hdr file.img', 'file file.img', 'primary', 'file.hdr'), - ('file.hdr file.img', 'file file.img file.hdr', 'primary', 'file.hdr'), - ('file.hdr file.img', 'file file', 'all', 'file.hdr file.img'), - ('file.hdr file.img', 'file file.img', 'all', 'file.hdr file.img'), - ('file.hdr file.img', 'file file.img file.hdr', 'all', 'file.hdr file.img'), - - # multiple files same prefix - ('file.hdr file.img file.nii', 'file', 'prefix', 'file'), - ('file.hdr file.img file.nii', 'file', 'primary', 'file.hdr file.nii'), - ('file.hdr file.img file.nii', 'file', 'all', 'file.hdr file.img file.nii'), - - # multiple files - ('file1.hdr file1.img file2.nii', 'file1', 'prefix', 'file1'), - ('file1.hdr file1.img file2.nii', 'file1', 'primary', 'file1.hdr'), - ('file1.hdr file1.img file2.nii', 'file1', 'all', 'file1.hdr file1.img'), - - ('file1.hdr file1.img file2.nii', 'file1 file2', 'prefix', 'file1 file2'), - ('file1.hdr file1.img file2.nii', 'file1 file2', 'primary', 'file1.hdr file2.nii'), - ('file1.hdr file1.img file2.nii', 'file1 file2', 'all', 'file1.hdr file1.img file2.nii'), - - # no file - ('file.nii', 'bag', 'prefix', ''), - ('file.nii', 'bag', 'primary', ''), - ('file.nii', 'bag', 'all', ''), - - # incomplete prefix - ('file.nii', 'fi', 'prefix', ''), - ('file.nii', 'fi', 'primary', ''), - ('file.nii', 'fi', 'all', ''), - ] - - - for to_create, paths, output, expected in tests: - with testdir(to_create.split()) as td: - - paths = paths.split() - expected = expected.split() - result = imglob.imglob(paths, output) - - assert sorted(result) == sorted(expected) - - -def test_imglob_shouldFail(): - - with pytest.raises(ValueError): - imglob.imglob([], 'bag') diff --git a/tests/test_scripts/test_fsl_abspath.py b/tests/test_scripts/test_fsl_abspath.py new file mode 100644 index 0000000000000000000000000000000000000000..1657128978f003dc551789e00d4f24f161c797ae --- /dev/null +++ b/tests/test_scripts/test_fsl_abspath.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# +# test_fsl_abspath.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + + +import os.path as op + +import fsl.scripts.fsl_abspath as fsl_abspath + +from tests import tempdir, CaptureStdout + + +def test_usage(): + assert fsl_abspath.main([]) != 0 + + +def test_fsl_abspath(): + + # fsl_abspath just calls os.path.realpath + + with tempdir() as td: + + oneup = op.dirname(td) + + # (input, expected) + tests = [ + ('file', f'{td}/file'), + ('./file', f'{td}/file'), + ('../file', f'{oneup}/file'), + + ('/file', '/file'), + ('/one/two/file', '/one/two/file'), + ] + + for input, expected in tests: + cap = CaptureStdout() + + with cap: + ret = fsl_abspath.main([input]) + + assert ret == 0 + assert cap.stdout.strip() == expected diff --git a/tests/test_scripts/test_imglob.py b/tests/test_scripts/test_imglob.py index 0f8e934509ce68359d17a8a8794bbaaa47f1a691..5ce2c2bc517dfcc00fa5c4be69886fd360de570e 100644 --- a/tests/test_scripts/test_imglob.py +++ b/tests/test_scripts/test_imglob.py @@ -1,6 +1,9 @@ #!/usr/bin/env python -import mock + +from unittest import mock + +import pytest import fsl.scripts.imglob as imglob @@ -8,7 +11,7 @@ from .. import testdir from .. import CaptureStdout -def test_imglob_script_shouldPass(): +def test_imglob_shouldPass1(): # (files to create, args, expected) tests = [ @@ -50,8 +53,81 @@ def test_imglob_script_shouldPass(): assert capture.stdout.strip().split() == expected.split() +def test_imglob_shouldPass2(): + + # (files to create, paths, output, expected) + tests = [ + + # normal usage, one file + ('file.hdr file.img', 'file', None, 'file'), + ('file.hdr file.img', 'file', 'prefix', 'file'), + ('file.hdr file.img', 'file', 'primary', 'file.hdr'), + ('file.hdr file.img', 'file', 'all', 'file.hdr file.img'), + + # incomplete file pair + ('file.hdr', 'file', 'prefix', 'file'), + ('file.hdr', 'file.hdr', 'prefix', 'file'), + ('file.hdr', 'file.img', 'prefix', 'file'), + ('file.hdr', 'file', 'primary', 'file.hdr'), + ('file.hdr', 'file.hdr', 'primary', 'file.hdr'), + ('file.hdr', 'file.img', 'primary', 'file.hdr'), + ('file.hdr', 'file', 'all', 'file.hdr'), + ('file.hdr', 'file.hdr', 'all', 'file.hdr'), + ('file.hdr', 'file.img', 'all', 'file.hdr'), + + # same file specified multiple times + ('file.hdr file.img', 'file file', 'prefix', 'file'), + ('file.hdr file.img', 'file file.img', 'prefix', 'file'), + ('file.hdr file.img', 'file file.img file.hdr', 'prefix', 'file'), + ('file.hdr file.img', 'file file', 'primary', 'file.hdr'), + ('file.hdr file.img', 'file file.img', 'primary', 'file.hdr'), + ('file.hdr file.img', 'file file.img file.hdr', 'primary', 'file.hdr'), + ('file.hdr file.img', 'file file', 'all', 'file.hdr file.img'), + ('file.hdr file.img', 'file file.img', 'all', 'file.hdr file.img'), + ('file.hdr file.img', 'file file.img file.hdr', 'all', 'file.hdr file.img'), + + # multiple files same prefix + ('file.hdr file.img file.nii', 'file', 'prefix', 'file'), + ('file.hdr file.img file.nii', 'file', 'primary', 'file.hdr file.nii'), + ('file.hdr file.img file.nii', 'file', 'all', 'file.hdr file.img file.nii'), + + # multiple files + ('file1.hdr file1.img file2.nii', 'file1', 'prefix', 'file1'), + ('file1.hdr file1.img file2.nii', 'file1', 'primary', 'file1.hdr'), + ('file1.hdr file1.img file2.nii', 'file1', 'all', 'file1.hdr file1.img'), + + ('file1.hdr file1.img file2.nii', 'file1 file2', 'prefix', 'file1 file2'), + ('file1.hdr file1.img file2.nii', 'file1 file2', 'primary', 'file1.hdr file2.nii'), + ('file1.hdr file1.img file2.nii', 'file1 file2', 'all', 'file1.hdr file1.img file2.nii'), + + # no file + ('file.nii', 'bag', 'prefix', ''), + ('file.nii', 'bag', 'primary', ''), + ('file.nii', 'bag', 'all', ''), + + # incomplete prefix + ('file.nii', 'fi', 'prefix', ''), + ('file.nii', 'fi', 'primary', ''), + ('file.nii', 'fi', 'all', ''), + ] + + + for to_create, paths, output, expected in tests: + with testdir(to_create.split()) as td: + + paths = paths.split() + expected = expected.split() + result = imglob.imglob(paths, output) + + assert sorted(result) == sorted(expected) + + + def test_imglob_script_shouldFail(): + with pytest.raises(ValueError): + imglob.imglob([], 'bag') + capture = CaptureStdout() with capture: diff --git a/tests/test_scripts/test_imln.py b/tests/test_scripts/test_imln.py new file mode 100644 index 0000000000000000000000000000000000000000..b9fea4e4fc00f9cf9b94370c801f4fb1d9a4606b --- /dev/null +++ b/tests/test_scripts/test_imln.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# +# test_imln.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import os +import os.path as op +from unittest import mock + +import pytest + +from fsl.utils.tempdir import tempdir +import fsl.utils.path as fslpath +import fsl.scripts.imln as imln + +from tests import touch + + +def test_usage(): + assert imln.main([]) != 0 + with mock.patch('sys.argv', []): + assert imln.main() != 0 + + +def test_imln(): + + # (files, command, expected links) + tests = [ + ('a.nii', 'a.nii link.nii', 'link.nii'), + ('a.nii', 'a link', 'link.nii'), + ('a.nii', 'a.nii link', 'link.nii'), + ('a.nii', 'a link.nii', 'link.nii'), + ('a.nii', 'a link.nii.gz', 'link.nii'), + ('a.nii.gz', 'a.nii.gz link.nii.gz', 'link.nii.gz'), + ('a.nii.gz', 'a link', 'link.nii.gz'), + ('a.nii.gz', 'a.nii.gz link', 'link.nii.gz'), + ('a.nii.gz', 'a link.nii.gz', 'link.nii.gz'), + ('a.nii.gz', 'a link.nii', 'link.nii.gz'), + + ('a.img a.hdr', 'a link', 'link.img link.hdr'), + ('a.img a.hdr', 'a link.img', 'link.img link.hdr'), + ('a.img a.hdr', 'a link.hdr', 'link.img link.hdr'), + ('a.img a.hdr', 'a.img link', 'link.img link.hdr'), + ('a.img a.hdr', 'a.hdr link', 'link.img link.hdr'), + ('a.img a.hdr', 'a.img link.img', 'link.img link.hdr'), + ('a.img a.hdr', 'a.hdr link.hdr', 'link.img link.hdr'), + ('a.img a.hdr', 'a.img link.hdr', 'link.img link.hdr'), + ('a.img a.hdr', 'a.hdr link.img', 'link.img link.hdr'), + ] + + + for files, command, explinks in tests: + with tempdir(): + files = files.split() + command = command.split() + explinks = explinks.split() + + for f in files: + touch(f) + + assert imln.main(command) == 0 + + assert sorted(os.listdir('.')) == sorted(files + explinks) + + for f, l in zip(sorted(files), sorted(explinks)): + assert op.islink(l) + assert op.isfile(f) and not op.islink(f) + assert op.realpath(l) == op.abspath(f) + + + # subdirs - imln currently only + # works with absolute paths (we + # make all paths absolute below) + tests = [ + ('dir/a.nii', 'dir/a dir/link', 'dir/link.nii'), + ('dir/a.img dir/a.hdr', 'dir/a dir/link', 'dir/link.img dir/link.hdr'), + ('dir/a.nii', 'dir/a link', 'link.nii'), + ('dir/a.img dir/a.hdr', 'dir/a link', 'link.img link.hdr'), + ('a.nii', 'a dir/link', 'dir/link.nii'), + ('a.img a.hdr', 'a dir/link', 'dir/link.img dir/link.hdr'), + ] + for files, command, explinks in tests: + with tempdir(): + files = files.split() + command = [op.abspath(c) for c in command.split()] + explinks = explinks.split() + + os.mkdir('dir') + + for f in files: + touch(f) + + assert imln.main(command) == 0 + + for f, l in zip(sorted(files), sorted(explinks)): + assert op.islink(l) + assert op.isfile(f) and not op.islink(f) + assert op.realpath(l) == op.abspath(f) + + # error cases + # (files, commnad) + tests = [ + ('a.img', 'a link'), + ('a.nii a.img a.hdr', 'a link'), + ] + for files, command in tests: + with tempdir(): + files = files.split() + command = command.split() + + for f in files: + touch(f) + + assert imln.main(command) != 0 + assert sorted(os.listdir('.')) == sorted(files) diff --git a/tests/test_scripts/test_imrm.py b/tests/test_scripts/test_imrm.py new file mode 100644 index 0000000000000000000000000000000000000000..d6682400a4d93696b996c5f78884be181ae04016 --- /dev/null +++ b/tests/test_scripts/test_imrm.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# +# test_imrm.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import os + +from fsl.utils.tempdir import tempdir +import fsl.scripts.imrm as imrm + +from tests import touch + +def test_imrm_usage(): + assert imrm.main([]) != 0 + + +def test_imrm(): + # (files present, command, expected) + tests = [ + ('a.nii', 'a', ''), + ('a.nii.gz', 'a', ''), + ('a.img a.hdr', 'a', ''), + ('a.img', 'a', ''), + ('a.hdr', 'a', ''), + ('a.nii b.nii', 'a', 'b.nii'), + ('a.nii b.nii', 'a b', ''), + ('a.nii b.nii', 'a b.nii', ''), + + # suffix doesn't have to be correct + ('a.nii.gz', 'a.nii', ''), + + # files don't exist -> no problem + ('a.nii', 'b', 'a.nii'), + ] + + for files, command, expected in tests: + with tempdir(): + + for f in files.split(): + touch(f) + + print('files', files) + print('command', command) + print('expected', expected) + + ret = imrm.main(('imrm ' + command).split()) + + assert ret == 0 + assert sorted(os.listdir()) == sorted(expected.split()) diff --git a/tests/test_scripts/test_imtest.py b/tests/test_scripts/test_imtest.py new file mode 100644 index 0000000000000000000000000000000000000000..819afa959175391d8b6fc44c986fe7b12e2021b3 --- /dev/null +++ b/tests/test_scripts/test_imtest.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# +# test_imtest.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import os +import os.path as op + +import fsl.utils.path as fslpath +from fsl.utils.tempdir import tempdir + +import fsl.scripts.imtest as imtest + +from tests import CaptureStdout, touch + + +def test_wrongargs(): + cap = CaptureStdout() + with cap: + assert imtest.main([]) == 0 + assert cap.stdout.strip() == '0' + + +def test_imtest(): + # (files, input, expected) + tests = [ + ('a.nii', 'a', '1'), + ('a.nii', 'a.nii', '1'), + ('a.nii', 'a.nii.gz', '1'), # imtest is suffix-agnostic + + ('a.img a.hdr', 'a', '1'), + ('a.img a.hdr', 'a.img', '1'), + ('a.img a.hdr', 'a.hdr', '1'), + ('a.img', 'a', '0'), + ('a.img', 'a.img', '0'), + ('a.img', 'a.hdr', '0'), + ('a.hdr', 'a', '0'), + ('a.hdr', 'a.img', '0'), + ('a.hdr', 'a.hdr', '0'), + + ('dir/a.nii', 'dir/a', '1'), + ('dir/a.img dir/a.hdr', 'dir/a', '1'), + ] + + for files, input, expected in tests: + with tempdir(): + for f in files.split(): + dirname = op.dirname(f) + if dirname != '': + os.makedirs(dirname, exist_ok=True) + touch(f) + + cap = CaptureStdout() + with cap: + assert imtest.main([input]) == 0 + + assert cap.stdout.strip() == expected + + # test that sym-links are + # followed correctly + with tempdir(): + touch('image.nii.gz') + os.symlink('image.nii.gz', 'link.nii.gz') + cap = CaptureStdout() + with cap: + assert imtest.main(['link']) == 0 + assert cap.stdout.strip() == '1' + + # sym-links in sub-directories + # (old imtest would not work + # in this scenario) + with tempdir(): + os.mkdir('subdir') + impath = op.join('subdir', 'image.nii.gz') + lnpath = op.join('subdir', 'link.nii.gz') + touch(impath) + os.symlink('image.nii.gz', lnpath) + cap = CaptureStdout() + with cap: + assert imtest.main([lnpath]) == 0 + assert cap.stdout.strip() == '1' diff --git a/tests/test_scripts/test_remove_ext.py b/tests/test_scripts/test_remove_ext.py new file mode 100644 index 0000000000000000000000000000000000000000..bb65a893af9d8dd6f3b6abcb5f60b1f7b4e576d9 --- /dev/null +++ b/tests/test_scripts/test_remove_ext.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# +# test_remove_ext.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + + +import fsl.scripts.remove_ext as remove_ext + +from tests import CaptureStdout + +def test_usage(): + assert remove_ext.main([]) != 0 + +def test_remove_ext(): + # (input, expected output) + tests = [ + ('a', 'a'), + ('a.nii', 'a'), + ('a.nii.gz', 'a'), + ('a.txt', 'a.txt'), + ('a.nii b.img c.hdr', 'a b c'), + ('a.nii b.img b.hdr', 'a b b'), + ('a b.img c.txt', 'a b c.txt'), + ('a.nii.gz b c.mnc', 'a b c'), + ] + + for input, expected in tests: + + cap = CaptureStdout() + with cap: + ret = remove_ext.main(input.split()) + + assert ret == 0 + + got = cap.stdout.split() + assert sorted(got) == sorted(expected.split()) diff --git a/tests/test_scripts/test_vest2text.py b/tests/test_scripts/test_vest2text.py new file mode 100644 index 0000000000000000000000000000000000000000..15cca4f5a22eb0d96cff4827ff4a3309489b4f05 --- /dev/null +++ b/tests/test_scripts/test_vest2text.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# +# test_vest2text.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import numpy as np + +import fsl.data.vest as fslvest +import fsl.scripts.Text2Vest as Text2Vest +import fsl.scripts.Vest2Text as Vest2Text + +from tests import tempdir + + +def test_usage(): + assert Vest2Text.main([]) == 0 + assert Text2Vest.main([]) == 0 + + +def test_Vest2Text(): + with tempdir(): + data = np.random.random((20, 10)) + vest = fslvest.generateVest(data) + + with open('data.vest', 'wt') as f: + f.write(vest) + + assert Vest2Text.main(['data.vest', 'data.txt']) == 0 + + got = np.loadtxt('data.txt') + + assert np.all(np.isclose(data, got)) + + +def test_Text2Vest(): + with tempdir(): + data = np.random.random((20, 10)) + + np.savetxt('data.txt', data) + + assert Text2Vest.main(['data.txt', 'data.vest']) == 0 + + got = fslvest.loadVestFile('data.vest', ignoreHeader=False) + + assert np.all(np.isclose(data, got)) diff --git a/tests/test_vest.py b/tests/test_vest.py index 407c502ef633880cfb072581be1a2a47e7a97581..a49f5fc3afb7c5469438a8facf70aed35a079ca1 100644 --- a/tests/test_vest.py +++ b/tests/test_vest.py @@ -6,16 +6,20 @@ # -import os.path as op -import shutil -import tempfile -import warnings +import os.path as op +import textwrap as tw +import io +import shutil +import tempfile +import warnings -import numpy as np -import pytest +import numpy as np +import pytest import fsl.data.vest as vest +from tests import tempdir + testfile1 = """%!VEST-LUT %%BeginInstance @@ -214,3 +218,75 @@ def test_loadVestLutFile(): finally: shutil.rmtree(testdir) + + +def test_generateVest(): + def readvest(vstr): + lines = vstr.split('\n') + nrows = [l for l in lines if 'NumPoints' in l][0] + ncols = [l for l in lines if 'NumWaves' in l][0] + nrows = int(nrows.split()[1]) + ncols = int(ncols.split()[1]) + data = '\n'.join(lines[3:]) + data = np.loadtxt(io.StringIO(data)).reshape((nrows, ncols)) + + return ((nrows, ncols), data) + + # shape, expectedshape + tests = [ + ((10, ), ( 1, 10)), + ((10, 1), (10, 1)), + (( 1, 10), ( 1, 10)), + (( 3, 5), ( 3, 5)), + (( 5, 3), ( 5, 3)) + ] + + for shape, expshape in tests: + data = np.random.random(shape) + vstr = vest.generateVest(data) + + gotshape, gotdata = readvest(vstr) + + data = data.reshape(expshape) + + assert expshape == gotshape + assert np.all(np.isclose(data, gotdata)) + + +def test_loadVestFile(): + def genvest(data, path, shapeOverride=None): + if shapeOverride is None: + nrows, ncols = data.shape + else: + nrows, ncols = shapeOverride + + with open(path, 'wt') as f: + f.write(f'/NumWaves {ncols}\n') + f.write(f'/NumPoints {nrows}\n') + f.write( '/Matrix\n') + + if np.issubdtype(data.dtype, np.integer): fmt = '%d' + else: fmt = '%0.12f' + + np.savetxt(f, data, fmt=fmt) + + with tempdir(): + data = np.random.randint(1, 100, (10, 20)) + genvest(data, 'data.vest') + assert np.all(data == vest.loadVestFile('data.vest')) + + data = np.random.random((20, 10)) + genvest(data, 'data.vest') + assert np.all(np.isclose(data, vest.loadVestFile('data.vest'))) + + # should pass + vest.loadVestFile('data.vest', ignoreHeader=False) + + # invalid VEST header + genvest(data, 'data.vest', (10, 20)) + + # default behaviour - ignore header + assert np.all(np.isclose(data, vest.loadVestFile('data.vest'))) + + with pytest.raises(ValueError): + vest.loadVestFile('data.vest', ignoreHeader=False)