diff --git a/.ci/test_template.sh b/.ci/test_template.sh index dd3e0022662614b764b5ac6331893f58c505c808..00bc034b1b76c8db636817e86f3f7820150a250c 100644 --- a/.ci/test_template.sh +++ b/.ci/test_template.sh @@ -28,7 +28,7 @@ if [ "$TEST_STYLE"x != "x" ]; then exit 0; fi # tests, and need $FSLDIR to be defined export FSLDIR=/fsl/ mkdir -p $FSLDIR/data/ -rsync -rv "fsldownload:data/atlases/" "$FSLDIR/data/atlases/" +rsync -rv "fsldownload:$FSL_ATLAS_DIR" "$FSLDIR/data/atlases/" # Finally, run the damned tests. TEST_OPTS="--cov-report= --cov-append" diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 792aee38e5d79796997101754ff0023b476f5aff..c07fcec57d0c65d3607255a8a5833642023b76f5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -72,6 +72,9 @@ stages: # - FSL_HOST: - Username@host to download FSL data from # (e.g. "paulmc@jalapeno.fmrib.ox.ac.uk") # +# - FSL_ATLAS_DIR: - Location of the FSL atlas data on +# FSL_HOST. +# # - TWINE_USERNAME: - Username to use when uploading to pypi # # - TWINE_PASSWORD: - Password to use when uploading to pypi diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 42815f289500ee9892044ca6276ccd8c9fb8496b..bd49656fa72a595585c318bbc14dd7df0389c3c0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,8 +2,8 @@ This document contains the ``fslpy`` release history in reverse chronological order. -2.0.0 (Under development) -------------------------- +2.0.0 (Friday March 20th 2019) +------------------------------ Added @@ -13,18 +13,23 @@ Added file/directory templates (Michiel Cottaar). * Simple built-in :mod:`.deprecated` decorator. * New :mod:`fsl.data.utils` module, which currently contains one function - :func:`.guessType`, which guess the data type of a file/directory path. + :func:`.guessType`, which guesses the data type of a file/directory path. +* New `.commonBase` function for finding the common prefix of a set of + file/directory paths. Changed ^^^^^^^ + * Removed support for Python 2.7 and 3.4. * Minimum required version of ``nibabel`` is now 2.3. * The :class:`.Image` class now fully delegates to ``nibabel`` for managing file handles. * The :class:`.GiftiMesh` class can now load surface files which contain - vertex data. + vertex data, and will accept surface files which end in ``.gii``, rather + than requiring files which end in ``.surf.gii``. +* The ``name`` property of :class:`.Mesh` instances can now be updated. Removed @@ -33,6 +38,14 @@ Removed * Many deprecated items removed. +Deprecated +^^^^^^^^^^ + + +* Deprecated the :func:`.loadIndexedImageFile` function, and the ``indexed`` + flag to the :class:`.Image` constructor. + + 1.13.3 (Friday February 8th 2019) --------------------------------- diff --git a/doc/fsl.utils.filetree.query.rst b/doc/fsl.utils.filetree.query.rst new file mode 100644 index 0000000000000000000000000000000000000000..d56577ba5087f3230c54b25573abc35df046043d --- /dev/null +++ b/doc/fsl.utils.filetree.query.rst @@ -0,0 +1,7 @@ +``fsl.utils.filetree.query`` +============================ + +.. automodule:: fsl.utils.filetree.query + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/fsl.utils.filetree.rst b/doc/fsl.utils.filetree.rst index 28ed519ced0fc52bee9c0b0f858953bfc6a91fb8..7dcdbf406eeb4caff66f25df69de9501f95de006 100644 --- a/doc/fsl.utils.filetree.rst +++ b/doc/fsl.utils.filetree.rst @@ -6,6 +6,7 @@ fsl.utils.filetree.filetree fsl.utils.filetree.parse + fsl.utils.filetree.query fsl.utils.filetree.utils .. automodule:: fsl.utils.filetree diff --git a/fsl/data/mesh.py b/fsl/data/mesh.py index 55b996069dfcb54b616bcc616b8b70eff90e02a7..f5ca20adea3747ca53945755f61c85dc28c539d1 100644 --- a/fsl/data/mesh.py +++ b/fsl/data/mesh.py @@ -241,6 +241,12 @@ class Mesh(notifier.Notifier, meta.Meta): return self.__name + @name.setter + def name(self, name): + """Set the name of this ``Mesh``. """ + self.__name = name + + @property def dataSource(self): """Returns the data source of this ``Mesh``. """ diff --git a/fsl/utils/filetree/__init__.py b/fsl/utils/filetree/__init__.py index 9d31383db6c525d926f9eeb5e558f530700a376c..736ad99983ab438191632a4514543a7f511719ef 100644 --- a/fsl/utils/filetree/__init__.py +++ b/fsl/utils/filetree/__init__.py @@ -277,4 +277,5 @@ of the short variable names defined in the __author__ = 'Michiel Cottaar <Michiel.Cottaar@ndcn.ox.ac.uk>' from .filetree import FileTree, register_tree, MissingVariable -from .parse import tree_directories +from .parse import tree_directories, list_all_trees +from .query import FileTreeQuery diff --git a/fsl/utils/filetree/filetree.py b/fsl/utils/filetree/filetree.py index 69c3d75b0c039e06d2d9e9e349763aa8e5532f73..9ba5fede6ad1d7f4ee6a9b9a24980941b6d7fc0a 100644 --- a/fsl/utils/filetree/filetree.py +++ b/fsl/utils/filetree/filetree.py @@ -1,5 +1,5 @@ from pathlib import Path, PurePath -from typing import Tuple, Optional, Dict, Any, Set +from typing import Tuple, Optional, List, Dict, Any, Set from copy import deepcopy from . import parse import pickle diff --git a/fsl/utils/filetree/parse.py b/fsl/utils/filetree/parse.py index c89b381fd38142158f78319c7b070a331bb875a5..11a8f1f221713b775caf35dfbfae7ba2f65102bb 100644 --- a/fsl/utils/filetree/parse.py +++ b/fsl/utils/filetree/parse.py @@ -1,7 +1,8 @@ +import glob import os.path as op from . import filetree from pathlib import PurePath -from typing import Tuple +from typing import Tuple, List import re @@ -26,6 +27,17 @@ def search_tree(name: str) -> str: raise ValueError("No file tree found for %s" % name) +def list_all_trees() -> List[str]: + """Return a list containing paths to all tree files that can be found in + :data:`tree_directories` + """ + trees = [] + for directory in tree_directories: + directory = op.abspath(directory) + trees.extend(glob.glob(op.join(directory, '*.tree'))) + return trees + + def read_line(line: str) -> Tuple[int, PurePath, str]: """ Parses line from the tree file diff --git a/fsl/utils/filetree/query.py b/fsl/utils/filetree/query.py new file mode 100644 index 0000000000000000000000000000000000000000..b1f1f46eddb6d007ab366852a4328df97a84547c --- /dev/null +++ b/fsl/utils/filetree/query.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python +# +# query.py - The FileTreeQuery class +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# Author: Michiel Cottaar <michiel.cottaar@.ndcn.ox.ac.uk> +# +"""This module contains the :class:`FileTreeQuery` class, which can be used to +search for files in a directory described by a `.FileTree`. A +``FileTreeQuery`` object returns :class:`Match` objects which each represent a +file that is described by the ``FileTree``, and which is present in the +directory. + +The following utility functions, used by the ``FileTreeQuery`` class, are also +defined in this module: + +.. autosummary:: + :nosignatures: + + scan + allVariables +""" + + +import logging +import collections + +import os.path as op +from typing import Dict, List, Tuple + +import numpy as np + +from . import FileTree + + +log = logging.getLogger(__name__) + + +class FileTreeQuery(object): + """The ``FileTreeQuery`` class uses a :class:`.FileTree` to search + a directory for files which match a specific query. + + A ``FileTreeQuery`` scans the contents of a directory which is described + by a :class:`.FileTree`, and identifies all file types (a.k.a. _templates_ + or _short names_) that are present, and the values of variables within each + short name that are present. The :meth:`query` method can be used to + retrieve files which match a specific short name, and variable values. + + The :meth:`query` method returns a multi-dimensional ``numpy.array`` + which contains :class:`Match` objects, where each dimension one + represents variable for the short name in question. + + Example usage:: + + >>> from fsl.utils.filetree import FileTree, FileTreeQuery + + >>> tree = FileTree.read('bids_raw', './my_bids_data') + >>> query = FileTreeQuery(tree) + + >>> query.axes('anat_image') + ['acq', 'ext', 'modality', 'participant', 'rec', 'run_index', + 'session'] + + >>> query.variables('anat_image') + {'acq': [None], + 'ext': ['.nii.gz'], + 'modality': ['T1w', 'T2w'], + 'participant': ['01', '02', '03'], + 'rec': [None], + 'run_index': [None, '01', '02', '03'], + 'session': [None]} + + >>> query.query('anat_image', participant='01') + array([[[[[[[Match(./my_bids_data/sub-01/anat/sub-01_T1w.nii.gz)], + [nan], + [nan], + [nan]]]], + + [[[[Match(./my_bids_data/sub-01/anat/sub-01_T2w.nii.gz)], + [nan], + [nan], + [nan]]]]]]], dtype=object) + """ + + + def __init__(self, tree): + """Create a ``FileTreeQuery``. The contents of the tree directory are + scanned via the :func:`scan` function, which may take some time for + large data sets. + + :arg tree: The :class:`.FileTree` object + """ + + # Find all files present in the directory + # (as Match objects), and find all variables, + # plus their values, and all short names, + # that are present in the directory. + matches = scan(tree) + allvars, shortnamevars = allVariables(tree, matches) + + # Now we are going to build a series of ND + # arrays to store Match objects. We create + # one array for each short name. Each axis + # in an array corresponds to a variable + # present in files of that short name type, + # and each position along an axis corresponds + # to one value of that variable. + # + # These arrays will be used to store and + # retrieve Match objects - given a short + # name and a set of variable values, we + # can quickly find the corresponding Match + # object (or objects). + + # matcharrays contains {shortname : ndarray} + # mappings, and varidxs contains + # {shortname : {varvalue : index}} mappings + matcharrays = {} + varidxs = {} + + for shortname in shortnamevars.keys(): + + snvars = shortnamevars[shortname] + snvarlens = [len(allvars[v]) for v in snvars] + + # An ND array for this short + # name. Each element is a + # Match object, or nan. + matcharray = np.zeros(snvarlens, dtype=np.object) + matcharray[:] = np.nan + + # indices into the match array + # for each variable value + snvaridxs = {} + for v in snvars: + snvaridxs[v] = {n : i for i, n in enumerate(allvars[v])} + + matcharrays[shortname] = matcharray + varidxs[ shortname] = snvaridxs + + # Populate the match arrays + for match in matches: + snvars = shortnamevars[match.short_name] + snvaridxs = varidxs[ match.short_name] + snarr = matcharrays[ match.short_name] + idx = [] + for var in snvars: + + val = match.variables[var] + idx.append(snvaridxs[var][val]) + + snarr[tuple(idx)] = match + + self.__allvars = allvars + self.__shortnamevars = shortnamevars + self.__matches = matches + self.__matcharrays = matcharrays + self.__varidxs = varidxs + + + def axes(self, short_name) -> List[str]: + """Returns a list containing the names of variables present in files + of the given ``short_name`` type, in the same order of the axes of + :class:`Match` arrays that are returned by the :meth:`query` method. + """ + return self.__shortnamevars[short_name] + + + def variables(self, short_name=None) -> Dict[str, List]: + """Return a dict of ``{variable : [values]}`` mappings. + This dict describes all variables and their possible values in + the tree. + + If a ``short_name`` is specified, only variables which are present in + files of that ``short_name`` type are returned. + """ + if short_name is None: + return {var : list(vals) for var, vals in self.__allvars.items()} + else: + varnames = self.__shortnamevars[short_name] + return {var : list(self.__allvars[var]) for var in varnames} + + + @property + def short_names(self) -> List[str]: + """Returns a list containing all short names of the ``FileTree`` that + are present in the directory. + """ + return list(self.__shortnamevars.keys()) + + + def query(self, short_name, asarray=False, **variables): + """Search for files of the given ``short_name``, which match + the specified ``variables``. All hits are returned for variables + that are unspecified. + + :arg short_name: Short name of files to search for. + + :arg asarray: If ``True``, the relevant :class:`Match` objects are + returned in a in a ND ``numpy.array`` where each + dimension corresponds to a variable for the + ``short_name`` in question (as returned by + :meth:`axes`). Otherwise (the default), they are + returned in a list. + + All other arguments are assumed to be ``variable=value`` pairs, + used to restrict which matches are returned. All values are returned + for variables that are not specified, or variables which are given a + value of ``'*'``. + + :returns: A list of ``Match`` objects, (or a ``numpy.array`` if + ``asarray=True``). + """ + + varnames = list(variables.keys()) + allvarnames = self.__shortnamevars[short_name] + varidxs = self.__varidxs[ short_name] + matcharray = self.__matcharrays[short_name] + slc = [] + + for var in allvarnames: + + if var in varnames: val = variables[var] + else: val = '*' + + # We're using np.newaxis to retain + # the full dimensionality of the + # array, so that the axis labels + # returned by the axes() method + # are valid. + if val == '*': slc.append(slice(None)) + else: slc.extend([np.newaxis, varidxs[var][val]]) + + result = matcharray[tuple(slc)] + + if asarray: return result + else: return [m for m in result.flat if isinstance(m, Match)] + + +class Match(object): + """A ``Match`` object represents a file with a name matching a template in + a ``FileTree``. The :func:`scan` function and :meth:`FileTree.query` + method both return ``Match`` objects. + """ + + + def __init__(self, filename, short_name, variables): + """Create a ``Match`` object. All arguments are added as attributes. + + :arg filename: name of existing file + :arg short_name: template identifier + :arg variables: Dictionary of ``{variable : value}`` mappings + containing all variables present in the file name. + """ + self.__filename = filename + self.__short_name = short_name + self.__variables = dict(variables) + + + @property + def filename(self): + return self.__filename + + + @property + def short_name(self): + return self.__short_name + + + @property + def variables(self): + return dict(self.__variables) + + + def __eq__(self, other): + return (isinstance(other, Match) and + self.filename == other.filename and + self.short_name == other.short_name and + self.variables == other.variables) + + + def __lt__(self, other): + return isinstance(other, Match) and self.filename < other.filename + + + def __le__(self, other): + return isinstance(other, Match) and self.filename <= other.filename + + + def __repr__(self): + """Returns a string representation of this ``Match``. """ + return 'Match({})'.format(self.filename) + + + def __str__(self): + """Returns a string representation of this ``Match``. """ + return repr(self) + + +def scan(tree : FileTree) -> List[Match]: + """Scans the directory of the given ``FileTree`` to find all files which + match a tree template. + + :return: list of :class:`Match` objects + """ + + matches = [] + for template in tree.templates: + for filename in tree.get_all(template, glob_vars='all'): + + if not op.isfile(filename): + continue + + variables = dict(tree.extract_variables(template, filename)) + + matches.append(Match(filename, template, variables)) + + for tree_name, sub_tree in tree.sub_trees.items(): + matches.extend(scan(sub_tree)) + + return matches + + +def allVariables( + tree : FileTree, + matches : List[Match]) -> Tuple[Dict[str, List], Dict[str, List]]: + """Identifies the ``FileTree`` variables which are actually represented + in files in the directory. + + :arg filetree: The ``FileTree``object + :arg matches: list of ``Match`` objects (e.g. as returned by :func:`scan`) + + :returns: a tuple containing two dicts: + + - A dict of ``{ variable : [values] }`` mappings containing all + variables and their possible values present in the given list + of ``Match`` objects. + + - A dict of ``{ short_name : [variables] }`` mappings, + containing the variables which are relevant to each short + name. + """ + allvars = collections.defaultdict(set) + allshortnames = collections.defaultdict(set) + + for m in matches: + for var, val in m.variables.items(): + allvars[ var] .add(val) + allshortnames[m.short_name].add(var) + + # allow us to compare None with strings + def key(v): + if v is None: return '' + else: return v + + allvars = {var : list(sorted(vals, key=key)) + for var, vals in allvars.items()} + allshortnames = {sn : list(sorted(vars)) + for sn, vars in allshortnames.items()} + + return allvars, allshortnames diff --git a/fsl/utils/filetree/utils.py b/fsl/utils/filetree/utils.py index e7dd4aad3347c678e32217e09eecf33d2ecdee35..0b82929491c9a8994d252f24704f66dff87f8411 100644 --- a/fsl/utils/filetree/utils.py +++ b/fsl/utils/filetree/utils.py @@ -158,6 +158,7 @@ def extract_variables(template, filename, known_vars=None): )) while '//' in sub_re: sub_re = sub_re.replace('//', '/') + sub_re = sub_re.replace('.', '\.') if re.match(sub_re, filename) is None: continue diff --git a/fsl/utils/path.py b/fsl/utils/path.py index 4190865a820dab8b52b8119ed305c0d17006a721..e45f82213f0b44495bd1c6e4573bb5ab169b7c1d 100644 --- a/fsl/utils/path.py +++ b/fsl/utils/path.py @@ -22,12 +22,14 @@ paths. getFileGroup removeDuplicates uniquePrefix + commonBase """ import os.path as op import os import glob +import operator class PathError(Exception): @@ -471,3 +473,30 @@ def uniquePrefix(path): hits = [h for h in hits if h.startswith(prefix)] return prefix + + +def commonBase(paths): + """Identifies the deepest common base directory shared by all files + in ``paths``. + + Raises a :exc:`PathError` if the paths have no common base. This will + never happen for absolute paths (as the base will be e.g. ``'/'``). + """ + + depths = [len(p.split(op.sep)) for p in paths] + base = max(zip(depths, paths), key=operator.itemgetter(0))[1] + last = base + + while True: + + base = op.split(base)[0] + + if base == last or len(base) == 0: + break + + last = base + + if all([p.startswith(base) for p in paths]): + return base + + raise PathError('No common base') diff --git a/tests/test_filetree/__init__.py b/tests/test_filetree/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/test_filetree/test_query.py b/tests/test_filetree/test_query.py new file mode 100644 index 0000000000000000000000000000000000000000..66e50c1f8d0c494b721e2e2ad0846e0629ef4b73 --- /dev/null +++ b/tests/test_filetree/test_query.py @@ -0,0 +1,438 @@ +#!/usr/bin/env python +# +# test_query.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import os +import glob +import shutil +import os.path as op +import contextlib +import textwrap as tw +import itertools as it + +from .. import testdir + +import fsl.utils.filetree as filetree +import fsl.utils.filetree.query as ftquery + + +_test_tree = """ +subj-{participant} + [ses-{session}] + T1w.nii.gz (T1w) + T2w.nii.gz (T2w) + {hemi}.{surf}.gii (surface) +""".strip() + +_subjs = ['01', '02', '03'] +_sess = ['1', '2'] +_hemis = ['L', 'R'] +_surfs = ['midthickness', 'pial', 'white'] + + +@contextlib.contextmanager +def _test_data(): + + files = [] + + for subj, ses in it.product(_subjs, _sess): + sesdir = op.join('subj-{}'.format(subj), 'ses-{}'.format(ses)) + files.append(op.join(sesdir, 'T1w.nii.gz')) + files.append(op.join(sesdir, 'T2w.nii.gz')) + + for hemi, surf in it.product(_hemis, _surfs): + files.append(op.join(sesdir, '{}.{}.gii'.format(hemi, surf))) + + with testdir(files): + with open('_test_tree.tree', 'wt') as f: + f.write(_test_tree) + yield + + +def _expected_matches(short_name, **kwargs): + + matches = [] + subjs = kwargs.get('participant', _subjs) + sess = kwargs.get('session', _sess) + surfs = kwargs.get('surf', _surfs) + hemis = kwargs.get('hemi', _hemis) + + for subj, ses in it.product(subjs, sess): + + sesdir = op.join('subj-{}'.format(subj), 'ses-{}'.format(ses)) + + if short_name in ('T1w', 'T2w'): + f = op.join(sesdir, '{}.nii.gz'.format(short_name)) + matches.append(ftquery.Match(f, + short_name, + {'participant' : subj, + 'session' : ses})) + + elif short_name == 'surface': + for hemi, surf in it.product(hemis, surfs): + f = op.join(sesdir, '{}.{}.gii'.format(hemi, surf)) + matches.append(ftquery.Match(f, + short_name, + {'participant' : subj, + 'session' : ses, + 'hemi' : hemi, + 'surf' : surf})) + + return matches + + +def _run_and_check_query(query, short_name, asarray=False, **vars): + + gotmatches = query.query( short_name, asarray=asarray, **vars) + expmatches = _expected_matches(short_name, **{k : [v] + for k, v + in vars.items()}) + + if not asarray: + assert len(gotmatches) == len(expmatches) + for got, exp in zip(sorted(gotmatches), sorted(expmatches)): + assert got == exp + else: + snvars = query.variables(short_name) + + assert len(snvars) == len(gotmatches.shape) + + for i, var in enumerate(sorted(snvars.keys())): + if var not in vars or vars[var] == '*': + assert len(snvars[var]) == gotmatches.shape[i] + else: + assert gotmatches.shape[i] == 1 + + for expmatch in expmatches: + slc = [] + for var in query.axes(short_name): + if var not in vars or vars[var] == '*': + vidx = snvars[var].index(expmatch.variables[var]) + slc.append(vidx) + else: + slc.append(0) + + +def test_query_properties(): + with _test_data(): + + tree = filetree.FileTree.read('_test_tree.tree', '.') + query = filetree.FileTreeQuery(tree) + + assert sorted(query.axes('T1w')) == ['participant', 'session'] + assert sorted(query.axes('T2w')) == ['participant', 'session'] + assert sorted(query.axes('surface')) == ['hemi', + 'participant', + 'session', + 'surf'] + assert sorted(query.short_names) == ['T1w', 'T2w', 'surface'] + + assert query.variables('T1w') == {'participant' : ['01', '02', '03'], + 'session' : ['1', '2']} + assert query.variables('T2w') == {'participant' : ['01', '02', '03'], + 'session' : ['1', '2']} + assert query.variables('surface') == {'participant' : ['01', '02', '03'], + 'session' : ['1', '2'], + 'surf' : ['midthickness', + 'pial', + 'white'], + 'hemi' : ['L', 'R']} + assert query.variables() == {'participant' : ['01', '02', '03'], + 'session' : ['1', '2'], + 'surf' : ['midthickness', + 'pial', + 'white'], + 'hemi' : ['L', 'R']} + + +def test_query(): + with _test_data(): + tree = filetree.FileTree.read('_test_tree.tree', '.') + query = filetree.FileTreeQuery(tree) + + _run_and_check_query(query, 'T1w') + _run_and_check_query(query, 'T1w', participant='01') + _run_and_check_query(query, 'T1w', session='2') + _run_and_check_query(query, 'T1w', participant='02', session='1') + _run_and_check_query(query, 'T2w') + _run_and_check_query(query, 'T2w', participant='01') + _run_and_check_query(query, 'T2w', session='2') + _run_and_check_query(query, 'T2w', participant='02', session='1') + _run_and_check_query(query, 'surface') + _run_and_check_query(query, 'surface', hemi='L') + _run_and_check_query(query, 'surface', surf='midthickness') + _run_and_check_query(query, 'surface', hemi='R', surf='pial') + _run_and_check_query(query, 'surface', participant='03', surf='pial') + _run_and_check_query(query, 'surface', participant='03', sssion='2') + + +def test_query_optional_var_folder(): + with _test_data(): + + # make subj-01 have no session sub-directories + for f in glob.glob(op.join('subj-01', 'ses-1', '*')): + shutil.move(f, 'subj-01') + shutil.rmtree(op.join('subj-01', 'ses-1')) + shutil.rmtree(op.join('subj-01', 'ses-2')) + + tree = filetree.FileTree.read('_test_tree.tree', '.') + query = filetree.FileTreeQuery(tree) + + assert query.variables()['session'] == [None, '1', '2'] + + m = query.query('T1w', participant='01') + assert len(m) == 1 + assert m[0].filename == op.join('subj-01', 'T1w.nii.gz') + + +def test_query_optional_var_filename(): + + treefile = tw.dedent(""" + sub-{subject} + img[-{modality}].nii.gz (image) + """).strip() + + files = [ + op.join('sub-01', 'img.nii.gz'), + op.join('sub-02', 'img-t1.nii.gz'), + op.join('sub-02', 'img-t2.nii.gz'), + op.join('sub-03', 'img-t1.nii.gz'), + op.join('sub-04', 'img.nii.gz')] + + with testdir(files): + with open('_test_tree.tree', 'wt') as f: + f.write(treefile) + + tree = filetree.FileTree.read('_test_tree.tree', '.') + query = filetree.FileTreeQuery(tree) + + qvars = query.variables() + + assert sorted(qvars.keys()) == ['modality', 'subject'] + assert qvars['subject'] == ['01', '02', '03', '04'] + assert qvars['modality'] == [None, 't1', 't2'] + + got = query.query('image', modality=None) + assert [m.filename for m in sorted(got)] == [ + op.join('sub-01', 'img.nii.gz'), + op.join('sub-04', 'img.nii.gz')] + + got = query.query('image', modality='t1') + assert [m.filename for m in sorted(got)] == [ + op.join('sub-02', 'img-t1.nii.gz'), + op.join('sub-03', 'img-t1.nii.gz')] + + got = query.query('image', modality='t2') + assert len(got) == 1 + assert got[0].filename == op.join('sub-02', 'img-t2.nii.gz') + + +def test_query_missing_files(): + with _test_data(): + + os.remove(op.join('subj-01', 'ses-1', 'T1w.nii.gz')) + os.remove(op.join('subj-02', 'ses-2', 'T2w.nii.gz')) + os.remove(op.join('subj-03', 'ses-1', 'L.white.gii')) + os.remove(op.join('subj-03', 'ses-1', 'L.midthickness.gii')) + os.remove(op.join('subj-03', 'ses-1', 'L.pial.gii')) + + tree = filetree.FileTree.read('_test_tree.tree', '.') + query = filetree.FileTreeQuery(tree) + + got = query.query('T1w', session='1') + assert [m.filename for m in sorted(got)] == [ + op.join('subj-02', 'ses-1', 'T1w.nii.gz'), + op.join('subj-03', 'ses-1', 'T1w.nii.gz')] + + got = query.query('T2w', session='2') + assert [m.filename for m in sorted(got)] == [ + op.join('subj-01', 'ses-2', 'T2w.nii.gz'), + op.join('subj-03', 'ses-2', 'T2w.nii.gz')] + + got = query.query('surface', session='1', hemi='L') + assert [m.filename for m in sorted(got)] == [ + op.join('subj-01', 'ses-1', 'L.midthickness.gii'), + op.join('subj-01', 'ses-1', 'L.pial.gii'), + op.join('subj-01', 'ses-1', 'L.white.gii'), + op.join('subj-02', 'ses-1', 'L.midthickness.gii'), + op.join('subj-02', 'ses-1', 'L.pial.gii'), + op.join('subj-02', 'ses-1', 'L.white.gii')] + + +def test_query_asarray(): + with _test_data(): + tree = filetree.FileTree.read('_test_tree.tree', '.') + query = filetree.FileTreeQuery(tree) + + _run_and_check_query(query, 'T1w', asarray=True) + _run_and_check_query(query, 'T1w', asarray=True, participant='01') + _run_and_check_query(query, 'T1w', asarray=True, session='2') + _run_and_check_query(query, 'T1w', asarray=True, participant='02', session='1') + _run_and_check_query(query, 'T2w', asarray=True) + _run_and_check_query(query, 'T2w', asarray=True, participant='01') + _run_and_check_query(query, 'T2w', asarray=True, session='2') + _run_and_check_query(query, 'T2w', asarray=True, participant='02', session='1') + _run_and_check_query(query, 'surface', asarray=True) + _run_and_check_query(query, 'surface', asarray=True, hemi='L') + _run_and_check_query(query, 'surface', asarray=True, surf='midthickness') + _run_and_check_query(query, 'surface', asarray=True, hemi='R', surf='pial') + _run_and_check_query(query, 'surface', asarray=True, participant='03', surf='pial') + _run_and_check_query(query, 'surface', asarray=True, participant='03', sssion='2') + + +def test_query_subtree(): + tree1 = tw.dedent(""" + subj-{participant} + T1w.nii.gz (T1w) + surf + ->surface (surfdir) + """) + tree2 = tw.dedent(""" + {hemi}.{surf}.gii (surface) + """) + + files = [ + op.join('subj-01', 'T1w.nii.gz'), + op.join('subj-01', 'surf', 'L.pial.gii'), + op.join('subj-01', 'surf', 'R.pial.gii'), + op.join('subj-01', 'surf', 'L.white.gii'), + op.join('subj-01', 'surf', 'R.white.gii'), + op.join('subj-02', 'T1w.nii.gz'), + op.join('subj-02', 'surf', 'L.pial.gii'), + op.join('subj-02', 'surf', 'R.pial.gii'), + op.join('subj-02', 'surf', 'L.white.gii'), + op.join('subj-02', 'surf', 'R.white.gii'), + op.join('subj-03', 'T1w.nii.gz'), + op.join('subj-03', 'surf', 'L.pial.gii'), + op.join('subj-03', 'surf', 'R.pial.gii'), + op.join('subj-03', 'surf', 'L.white.gii'), + op.join('subj-03', 'surf', 'R.white.gii')] + + with testdir(files): + with open('tree1.tree', 'wt') as f: f.write(tree1) + with open('surface.tree', 'wt') as f: f.write(tree2) + + tree = filetree.FileTree.read('tree1.tree', '.') + query = filetree.FileTreeQuery(tree) + + assert sorted(query.short_names) == ['T1w', 'surface'] + + qvars = query.variables() + assert sorted(qvars.keys()) == ['hemi', 'participant', 'surf'] + assert qvars['hemi'] == ['L', 'R'] + assert qvars['participant'] == ['01', '02', '03'] + assert qvars['surf'] == ['pial', 'white'] + + qvars = query.variables('T1w') + assert sorted(qvars.keys()) == ['participant'] + assert qvars['participant'] == ['01', '02', '03'] + + qvars = query.variables('surface') + assert sorted(qvars.keys()) == ['hemi', 'participant', 'surf'] + assert qvars['hemi'] == ['L', 'R'] + assert qvars['participant'] == ['01', '02', '03'] + assert qvars['surf'] == ['pial', 'white'] + + got = query.query('T1w') + assert [m.filename for m in sorted(got)] == [ + op.join('subj-01', 'T1w.nii.gz'), + op.join('subj-02', 'T1w.nii.gz'), + op.join('subj-03', 'T1w.nii.gz')] + + got = query.query('T1w', participant='01') + assert [m.filename for m in sorted(got)] == [ + op.join('subj-01', 'T1w.nii.gz')] + + got = query.query('surface') + assert [m.filename for m in sorted(got)] == [ + op.join('subj-01', 'surf', 'L.pial.gii'), + op.join('subj-01', 'surf', 'L.white.gii'), + op.join('subj-01', 'surf', 'R.pial.gii'), + op.join('subj-01', 'surf', 'R.white.gii'), + op.join('subj-02', 'surf', 'L.pial.gii'), + op.join('subj-02', 'surf', 'L.white.gii'), + op.join('subj-02', 'surf', 'R.pial.gii'), + op.join('subj-02', 'surf', 'R.white.gii'), + op.join('subj-03', 'surf', 'L.pial.gii'), + op.join('subj-03', 'surf', 'L.white.gii'), + op.join('subj-03', 'surf', 'R.pial.gii'), + op.join('subj-03', 'surf', 'R.white.gii')] + + got = query.query('surface', hemi='L') + assert [m.filename for m in sorted(got)] == [ + op.join('subj-01', 'surf', 'L.pial.gii'), + op.join('subj-01', 'surf', 'L.white.gii'), + op.join('subj-02', 'surf', 'L.pial.gii'), + op.join('subj-02', 'surf', 'L.white.gii'), + op.join('subj-03', 'surf', 'L.pial.gii'), + op.join('subj-03', 'surf', 'L.white.gii')] + + got = query.query('surface', surf='white') + assert [m.filename for m in sorted(got)] == [ + op.join('subj-01', 'surf', 'L.white.gii'), + op.join('subj-01', 'surf', 'R.white.gii'), + op.join('subj-02', 'surf', 'L.white.gii'), + op.join('subj-02', 'surf', 'R.white.gii'), + op.join('subj-03', 'surf', 'L.white.gii'), + op.join('subj-03', 'surf', 'R.white.gii')] + + +def test_scan(): + + with _test_data(): + tree = filetree.FileTree.read('_test_tree.tree', '.') + gotmatches = ftquery.scan(tree) + + expmatches = [] + + for subj, ses in it.product(_subjs, _sess): + + sesdir = op.join('subj-{}'.format(subj), 'ses-{}'.format(ses)) + t1wf = op.join(sesdir, 'T1w.nii.gz') + t2wf = op.join(sesdir, 'T2w.nii.gz') + + expmatches.append(ftquery.Match(t1wf, 'T1w', {'participant' : subj, + 'session' : ses})) + expmatches.append(ftquery.Match(t2wf, 'T2w', {'participant' : subj, + 'session' : ses})) + + for hemi, surf in it.product(_hemis, _surfs): + surff = op.join(sesdir, '{}.{}.gii'.format(hemi, surf)) + + expmatches.append(ftquery.Match(surff, + 'surface', + {'participant' : subj, + 'session' : ses, + 'surf' : surf, + 'hemi' : hemi})) + + + assert len(gotmatches) == len(expmatches) + + for got, exp in zip(sorted(gotmatches), sorted(expmatches)): + assert got.filename == exp.filename + assert got.short_name == exp.short_name + assert got.variables == exp.variables + + +def test_allVariables(): + with _test_data(): + tree = filetree.FileTree.read('_test_tree.tree', '.') + matches = ftquery.scan(tree) + qvars, snames = ftquery.allVariables(tree, matches) + + expqvars = { + 'participant' : _subjs, + 'session' : _sess, + 'surf' : _surfs, + 'hemi' : _hemis} + expsnames = { + 'T1w' : ['participant', 'session'], + 'T2w' : ['participant', 'session'], + 'surface' : ['hemi', 'participant', 'session', 'surf']} + + assert qvars == expqvars + assert snames == expsnames diff --git a/tests/test_fsl_utils_path.py b/tests/test_fsl_utils_path.py index 0820f4ed642c9499047b6a02f811027d52522d15..f5e798377e5e45dff555c2aa90d2b41fa5ae3ed9 100644 --- a/tests/test_fsl_utils_path.py +++ b/tests/test_fsl_utils_path.py @@ -1356,3 +1356,32 @@ def test_uniquePrefix(): finally: shutil.rmtree(workdir) + + +def test_commonBase(): + + tests = [ + ('/', + ['/a/b/c', + '/d/e', + '/f/g/h/i']), + ('/a', + ['/a/b/c', + '/a/d/e/f/g', + '/a/d/h/g/h/i']), + ('a', + ['a/b/c/d', + 'a/e/f/g/h', + 'a/i/j/k/']) + ] + for exp, paths in tests: + assert fslpath.commonBase(paths) == exp + + failtests = [ + ['a/b/c', 'd/e/f'], + ['/a/b/c', 'd/e/f'], + ['a', 'b/c/d']] + + for ft in failtests: + with pytest.raises(fslpath.PathError): + fslpath.commonBase(ft)