diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8d0ae06b614a8dbebd0c3c9b7a483ba596a600c9..49beaf83d423cd3a0e784b2af0bb8cebd2a745c7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -134,7 +134,7 @@ variables: rules: - if: $SKIP_TESTS != null when: never - - if: $CI_COMMIT_MESSAGE =~ /skip-tests/ + - if: $CI_COMMIT_MESSAGE =~ /\[skip-tests\]/ when: never - if: $CI_PIPELINE_SOURCE == "merge_request_event" when: on_success diff --git a/CHANGELOG.rst b/CHANGELOG.rst index aeaf86356c9bd85834b13a825c8ecb20a2658fa8..b9fab5d092b766e33361a50ee96fb52c1f921953 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,17 @@ This document contains the ``fslpy`` release history in reverse chronological order. +3.15.0 (Under development) +-------------------------- + + +Changed +^^^^^^^ + + +* All metadata stored in GIfTI files is now copied by :class:`.GiftiMesh` + instances into their :class:`.Meta` store (!416). + 3.14.1 (Thursday 31st August 2023) ---------------------------------- diff --git a/fsl/data/cifti.py b/fsl/data/cifti.py index ecd193eb9854b2c3198954852f8f9cfc76706bf0..08f67f3688a01a3cdaa2ab45ad2f647279a42eba 100644 --- a/fsl/data/cifti.py +++ b/fsl/data/cifti.py @@ -440,11 +440,12 @@ class BrainStructure(object): secondary_str = 'AnatomicalStructureSecondary' primary = "other" secondary = None - for meta in [gifti_obj] + gifti_obj.darrays: - if primary_str in meta.meta.metadata: - primary = meta.meta.metadata[primary_str] - if secondary_str in meta.meta.metadata: - secondary = meta.meta.metadata[secondary_str] + + for obj in [gifti_obj] + gifti_obj.darrays: + if primary_str in obj.meta: + primary = obj.meta[primary_str] + if secondary_str in obj.meta: + secondary = obj.meta[secondary_str] anatomy = cls.from_string(primary, issurface=True) anatomy.secondary = None if secondary is None else secondary.lower() return anatomy diff --git a/fsl/data/gifti.py b/fsl/data/gifti.py index 077b7c4e14eea3c84064b45d9d69f9253a5ed742..0ff4c8dd5848e78dd7704a01d6e6db9474ffaca4 100644 --- a/fsl/data/gifti.py +++ b/fsl/data/gifti.py @@ -101,9 +101,24 @@ class GiftiMesh(fslmesh.Mesh): for i, v in enumerate(vertices): if i == 0: key = infile - else: key = '{}_{}'.format(infile, i) + else: key = f'{infile}_{i}' self.addVertices(v, key, select=(i == 0), fixWinding=fixWinding) - self.setMeta(infile, surfimg) + self.meta[infile] = surfimg + + # Copy all metadata entries for the GIFTI image + for k, v in surfimg.meta.items(): + self.meta[k] = v + + # and also for each GIFTI data array - triangles + # are stored under "faces", and pointsets are + # stored under "vertices"/[0,1,2...] (as there may + # be multiple pointsets in a file) + self.meta['vertices'] = {} + for i, arr in enumerate(surfimg.darrays): + if arr.intent == constants.NIFTI_INTENT_POINTSET: + self.meta['vertices'][i] = dict(arr.meta) + elif arr.intent == constants.NIFTI_INTENT_TRIANGLE: + self.meta['faces'] = dict(arr.meta) if vdata is not None: self.addVertexData(infile, vdata) @@ -130,7 +145,7 @@ class GiftiMesh(fslmesh.Mesh): continue self.addVertices(vertices[0], sfile, select=False) - self.setMeta(sfile, surfimg) + self.meta[sfile] = surfimg def loadVertices(self, infile, key=None, *args, **kwargs): @@ -154,10 +169,10 @@ class GiftiMesh(fslmesh.Mesh): for i, v in enumerate(vertices): if i == 0: key = infile - else: key = '{}_{}'.format(infile, i) + else: key = f'{infile}_{i}' vertices[i] = self.addVertices(v, key, *args, **kwargs) - self.setMeta(infile, surfimg) + self.meta[infile] = surfimg return vertices @@ -221,12 +236,12 @@ def loadGiftiMesh(filename): vdata = [d for d in gimg.darrays if d.intent not in (pscode, tricode)] if len(triangles) != 1: - raise ValueError('{}: GIFTI surface files must contain ' - 'exactly one triangle array'.format(filename)) + raise ValueError(f'{filename}: GIFTI surface files must ' + 'contain exactly one triangle array') if len(pointsets) == 0: - raise ValueError('{}: GIFTI surface files must contain ' - 'at least one pointset array'.format(filename)) + raise ValueError(f'{filename}: GIFTI surface files must ' + 'contain at least one pointset array') vertices = [ps.data for ps in pointsets] indices = np.atleast_2d(triangles[0].data) @@ -276,14 +291,14 @@ def prepareGiftiVertexData(darrays, filename=None): intents = {d.intent for d in darrays} if len(intents) != 1: - raise ValueError('{} contains multiple (or no) intents' - ': {}'.format(filename, intents)) + raise ValueError(f'{filename} contains multiple ' + f'(or no) intents: {intents}') intent = intents.pop() if intent in (constants.NIFTI_INTENT_POINTSET, constants.NIFTI_INTENT_TRIANGLE): - raise ValueError('{} contains surface data'.format(filename)) + raise ValueError(f'{filename} contains surface data') # Just a single array - return it as-is. # n.b. Storing (M, N) data in a single @@ -298,8 +313,8 @@ def prepareGiftiVertexData(darrays, filename=None): vdata = [d.data for d in darrays] if any([len(d.shape) != 1 for d in vdata]): - raise ValueError('{} contains one or more non-vector ' - 'darrays'.format(filename)) + raise ValueError(f'{filename} contains one or ' + 'more non-vector darrays') vdata = np.vstack(vdata).T vdata = vdata.reshape(vdata.shape[0], -1) @@ -374,7 +389,7 @@ def relatedFiles(fname, ftypes=None): def searchhcp(match, ftype): prefix, space = match - template = '{}.*.{}{}'.format(prefix, space, ftype) + template = f'{prefix}.*.{space}{ftype}' return glob.glob(op.join(dirname, template)) # BIDS style - extract all entities (kv diff --git a/fsl/tests/__init__.py b/fsl/tests/__init__.py index edd0f7b46b276ba03af3766973e0d5a9ec9b4a67..c96143037145baea9b2c0d177555120794580d28 100644 --- a/fsl/tests/__init__.py +++ b/fsl/tests/__init__.py @@ -147,6 +147,8 @@ def testdir(contents=None, suffix=""): shutil.rmtree(self.testdir) return ctx(contents) +testdir.__test__ = False + def make_dummy_files(paths): """Creates dummy files for all of the given paths. """ diff --git a/fsl/tests/test_meta.py b/fsl/tests/test_meta.py index 791f1873b0b070e8b3a61c78aa0e8ae58917f541..a8221b2af6a15c35b36e474883a7a5b2ea8a8d04 100644 --- a/fsl/tests/test_meta.py +++ b/fsl/tests/test_meta.py @@ -19,6 +19,8 @@ def test_meta(): for k, v in data.items(): assert m.getMeta(k) == v + assert m.meta == data + assert list(data.keys()) == list(m.metaKeys()) assert list(data.values()) == list(m.metaValues()) assert list(data.items()) == list(m.metaItems()) diff --git a/fsl/utils/meta.py b/fsl/utils/meta.py index 30d18a567021f014f9ae41fcbcb381f43b47155d..2eccff01ea2cf36b070f1839f6f8a69494f14be4 100644 --- a/fsl/utils/meta.py +++ b/fsl/utils/meta.py @@ -7,12 +7,9 @@ """This module provides the :class:`Meta` class. """ -import collections - - -class Meta(object): - """The ``Meta`` class is intended to be used as a mixin for other classes. It - is simply a wrapper for a dictionary of key-value pairs. +class Meta: + """The ``Meta`` class is intended to be used as a mixin for other classes. + It is simply a wrapper for a dictionary of key-value pairs. It has a handful of methods allowing you to add and access additional metadata associated with an object. @@ -20,6 +17,7 @@ class Meta(object): .. autosummary:: :nosignatures: + meta metaKeys metaValues metaItems @@ -32,11 +30,17 @@ class Meta(object): """Initialises a ``Meta`` instance. """ new = super(Meta, cls).__new__(cls) - new.__meta = collections.OrderedDict() + new.__meta = {} return new + @property + def meta(self): + """Return a reference to the metadata dictionary. """ + return self.__meta + + def metaKeys(self): """Returns the keys contained in the metadata dictionary (``dict.keys``).