diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7a11efa4b0ea2f997f135b14cc084511ca701da6..6e1d981ce2f7ea6176dffd2e29a9dc70b7d0495a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,8 @@ Changed * The :class:`.TaskThread` now allows an error handler function to be specified, which is run on the :mod:`.idle` loop. +* The :func:`.bids.loadMetadata` function no long resolves sym-links when + determining whether a file is contained within a BIDS data set. Deprecated diff --git a/fsl/utils/bids.py b/fsl/utils/bids.py index cae75cb4f61a806faa7de5a7d83dc04cb44e7f83..eb36a5dc803f06aa2d4a7bd23cde068513c92253 100644 --- a/fsl/utils/bids.py +++ b/fsl/utils/bids.py @@ -18,6 +18,7 @@ All of the other functions in this module should not be considered part of the public API. +.. see:: https://bids-specification.readthedocs.io/en/stable/ .. note:: The `pybids <https://bids-standard.github.io/pybids/>`_ library is a more suitable choice if you are after a more robust and featured @@ -36,8 +37,8 @@ import fsl.utils.path as fslpath class BIDSFile(object): - """The ``BIDSFile`` class parses and stores the entities and suffix contained - in a BIDS file. See the :func:`parseFilename` function. + """The ``BIDSFile`` class parses and stores the entities and suffix + contained in a BIDS file. See the :func:`parseFilename` function. The :meth:`match` method can be used to compare two ``BIDSFile`` instances. @@ -91,15 +92,9 @@ class BIDSFile(object): def parseFilename(filename): - """Parses a BIDS-like file name. The file name must consist of zero or more - "entities" (alpha-numeric ``name-value`` pairs), a "suffix", all separated - by underscores, and a regular file extension. For example, the following - file:: - - sub-01_ses-01_task-stim_bold.nii.gz - - has suffix ``bold``, entities ``sub=01``, ``ses=01`` and ``task=stim``, and - extension ``.nii.gz``. + """Parses a BIDS-like file name, returning the entities and suffix encoded + in the name. See the :func:`isBIDSFile` function for an explanation of + what is considered to be a valid BIDS file name. .. note:: This function assumes that no period (``.``) characters occur in the body of a BIDS filename. @@ -161,11 +156,24 @@ def inBIDSDir(filename): def isBIDSFile(filename, strict=True): """Returns ``True`` if ``filename`` looks like a BIDS image or JSON file. + A BIDS file name must consist of zero or more "entities" (alpha-numeric + ``name-value`` pairs), a "suffix", all separated by underscores, and a + regular file extension. For example, the following file:: + + sub-01_ses-01_task-stim_bold.nii.gz + + has suffix ``bold``, entities ``sub=01``, ``ses=01`` and ``task=stim``, and + extension ``.nii.gz``. + :arg filename: Name of file to check :arg strict: If ``True`` (the default), the file must be within a BIDS dataset directory, as defined by :func:`inBIDSDir`. """ + # Zero or more entities because sidecar files + # do not necessarily need to contain any + # entities (e.g. ``T1w.json`` is valid) + name = op.basename(filename) pattern = r'([a-z0-9]+-[a-z0-9]+_)*([a-z0-9])+\.(.+)' flags = re.ASCII | re.IGNORECASE @@ -189,7 +197,7 @@ def loadMetadata(filename): ``filename`` """ - filename = op.realpath(op.abspath(filename)) + filename = op.abspath(filename) bfile = BIDSFile(filename) dirname = op.dirname(filename) prevdir = filename diff --git a/tests/test_bids.py b/tests/test_bids.py index a5facaf8ea2727a23e89471bc13e57dee25745f4..4a2bc1e10c76bf03027962eb8cb2f7c4a4551bd8 100644 --- a/tests/test_bids.py +++ b/tests/test_bids.py @@ -18,8 +18,11 @@ import fsl.utils.bids as fslbids def test_parseFilename(): - with pytest.raises(ValueError): - fslbids.parseFilename('bad_file.txt') + badtests = ['bad_file.txt'] + + for test in badtests: + with pytest.raises(ValueError): + fslbids.parseFilename(test) tests = [ ('sub-01_ses-01_t1w.nii.gz', @@ -105,3 +108,41 @@ def test_loadMetadata(): assert fslbids.loadMetadata(t1) == {**meta2, **meta1} json4.write_text(json.dumps(meta4)) assert fslbids.loadMetadata(t1) == {**meta4, **meta2, **meta1} + + + +def test_loadMetadata_symlinked(): + ddreal = Path('a') + t1real = Path('b') + j1real = Path('c') + j2real = Path('d') + j3real = Path('e') + j4real = Path('f') + dd = Path('data/dataset_description.json') + t1 = Path('data/sub-01/func/sub-01_task-stim_bold.nii.gz') + json1 = Path('data/sub-01/func/sub-01_task-stim_bold.json') + json2 = Path('data/sub-01/sub-01_bold.json') + json3 = Path('data/sub-01_t1w.json') + json4 = Path('data/sub-01/task-stim_bold.json') + meta1 = {'a' : '1', 'b' : '2'} + meta2 = {'a' : '10', 'c' : '3'} + meta3 = {'a' : '109', 'b' : '99'} + meta4 = {'c' : '9', 'd' : '5'} + + with tempdir(): + ddreal.touch() + t1real.touch() + j1real.write_text(json.dumps(meta1)) + j2real.write_text(json.dumps(meta2)) + j3real.write_text(json.dumps(meta3)) + j4real.write_text(json.dumps(meta4)) + + Path(op.dirname(t1)).mkdir(parents=True) + dd .symlink_to(op.join('..', ddreal)) + t1 .symlink_to(op.join('..', '..', '..', t1real)) + json1.symlink_to(op.join('..', '..', '..', j1real)) + json2.symlink_to(op.join('..', '..', j2real)) + json3.symlink_to(op.join('..', j3real)) + json4.symlink_to(op.join('..', '..', j4real)) + + assert fslbids.loadMetadata(t1) == {**meta4, **meta2, **meta1}