From dff774e2d89b07a0b40b44219391e906616ef5f1 Mon Sep 17 00:00:00 2001
From: mathiasg <mathiasg@stanford.edu>
Date: Mon, 6 Jul 2020 09:32:33 -0400
Subject: [PATCH 1/9] RF: Allow kwargs within ``to_filename`` and
 ``instance_to_filename`` methods

---
 nibabel/filebasedimages.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/nibabel/filebasedimages.py b/nibabel/filebasedimages.py
index 006b70d615..f610e87543 100644
--- a/nibabel/filebasedimages.py
+++ b/nibabel/filebasedimages.py
@@ -315,7 +315,7 @@ def filespec_to_file_map(klass, filespec):
     def filespec_to_files(klass, filespec):
         return klass.filespec_to_file_map(filespec)
 
-    def to_filename(self, filename):
+    def to_filename(self, filename, **kwargs):
         """ Write image to files implied by filename string
 
         Parameters
@@ -381,7 +381,7 @@ def make_file_map(klass, mapping=None):
     load = from_filename
 
     @classmethod
-    def instance_to_filename(klass, img, filename):
+    def instance_to_filename(klass, img, filename, **kwargs):
         """ Save `img` in our own format, to name implied by `filename`
 
         This is a class method
@@ -394,7 +394,7 @@ def instance_to_filename(klass, img, filename):
            Filename, implying name to which to save image.
         """
         img = klass.from_image(img)
-        img.to_filename(filename)
+        img.to_filename(filename, **kwargs)
 
     @classmethod
     def from_image(klass, img):

From f78d7f1b54b2be05bf543152f7fe406b978c3bf8 Mon Sep 17 00:00:00 2001
From: mathiasg <mathiasg@stanford.edu>
Date: Mon, 6 Jul 2020 09:33:46 -0400
Subject: [PATCH 2/9] ENH: Add option to allow inferring of intent code based
 on output file suffixes

---
 nibabel/cifti2/cifti2.py                | 50 ++++++++++++++++++++++---
 nibabel/cifti2/tests/test_new_cifti2.py | 34 ++++++-----------
 2 files changed, 56 insertions(+), 28 deletions(-)

diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py
index 0b2b7f2a9a..2cc6931de7 100644
--- a/nibabel/cifti2/cifti2.py
+++ b/nibabel/cifti2/cifti2.py
@@ -89,6 +89,23 @@ class Cifti2HeaderError(Exception):
                           'CIFTI_STRUCTURE_THALAMUS_LEFT',
                           'CIFTI_STRUCTURE_THALAMUS_RIGHT')
 
+# "Standard CIFTI Mapping Combinations" within CIFTI-2 spec
+# https://www.nitrc.org/forum/attachment.php?attachid=341&group_id=454&forum_id=1955
+CIFTI_EXTENSIONS_TO_INTENTS = {
+    '.dconn': 'NIFTI_INTENT_CONNECTIVITY_DENSE',
+    '.dtseries': 'NIFTI_INTENT_CONNECTIVITY_DENSE_SERIES',
+    '.pconn': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED',
+    '.ptseries': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SERIES',
+    '.dscalar': 'NIFTI_INTENT_CONNECTIVITY_DENSE_SCALARS',
+    '.dlabel': 'NIFTI_INTENT_CONNECTIVITY_DENSE_LABELS',
+    '.pscalar': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SCALAR',
+    '.pdconn': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_DENSE',
+    '.dpconn': 'NIFTI_INTENT_CONNECTIVITY_DENSE_PARCELLATED',
+    '.pconnseries': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_PARCELLATED_SERIES',
+    '.pconnscalar': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_PARCELLATED_SCALAR',
+    '.dfan': 'NIFTI_INTENT_CONNECTIVITY_DENSE_SERIES',
+}
+
 
 def _value_if_klass(val, klass):
     if val is None or isinstance(val, klass):
@@ -1466,11 +1483,7 @@ def to_file_map(self, file_map=None):
             raise ValueError(
                 f"Dataobj shape {self._dataobj.shape} does not match shape "
                 f"expected from CIFTI-2 header {self.header.matrix.get_data_shape()}")
-        # if intent code is not set, default to unknown CIFTI
-        if header.get_intent()[0] == 'none':
-            header.set_intent('NIFTI_INTENT_CONNECTIVITY_UNKNOWN')
-        data = reshape_dataobj(self.dataobj,
-                               (1, 1, 1, 1) + self.dataobj.shape)
+        data = reshape_dataobj(self.dataobj, (1, 1, 1, 1) + self.dataobj.shape)
         # If qform not set, reset pixdim values so Nifti2 does not complain
         if header['qform_code'] == 0:
             header['pixdim'][:4] = 1
@@ -1509,6 +1522,33 @@ def get_data_dtype(self):
     def set_data_dtype(self, dtype):
         self._nifti_header.set_data_dtype(dtype)
 
+    def to_filename(self, filename, infer_intent=False):
+        """
+        Ensures NIfTI header intent code is set prior to saving.
+
+        Parameters
+        ----------
+        infer_intent : boolean, optional
+            If ``True``, attempt to infer and set intent code based on filename suffix.
+        """
+        header = self._nifti_header
+        if infer_intent:
+            # try to infer intent code based on filename suffix
+            intent = _infer_intent_from_filename(filename)
+            if intent is not None:
+                header.set_intent(intent)
+        # if intent code is not set, default to unknown
+        if header.get_intent()[0] == 'none':
+            header.set_intent('NIFTI_INTENT_CONNECTIVITY_UNKNOWN')
+        super().to_filename(filename)
+
+
+def _infer_intent_from_filename(filename):
+    """Parses output filename for common suffixes and fetches corresponding intent code"""
+    from pathlib import Path
+    ext = Path(filename).suffixes[0]
+    return CIFTI_EXTENSIONS_TO_INTENTS.get(ext)
+
 
 load = Cifti2Image.from_filename
 save = Cifti2Image.instance_to_filename
diff --git a/nibabel/cifti2/tests/test_new_cifti2.py b/nibabel/cifti2/tests/test_new_cifti2.py
index 65ef95c316..5ee6f4a7c3 100644
--- a/nibabel/cifti2/tests/test_new_cifti2.py
+++ b/nibabel/cifti2/tests/test_new_cifti2.py
@@ -237,10 +237,9 @@ def test_dtseries():
     hdr = ci.Cifti2Header(matrix)
     data = np.random.randn(13, 10)
     img = ci.Cifti2Image(data, hdr)
-    img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_DENSE_SERIES')
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.dtseries.nii')
+        ci.save(img, 'test.dtseries.nii', infer_intent=True)
         img2 = nib.load('test.dtseries.nii')
         assert img2.nifti_header.get_intent()[0] == 'ConnDenseSeries'
         assert isinstance(img2, ci.Cifti2Image)
@@ -281,10 +280,9 @@ def test_dlabel():
     hdr = ci.Cifti2Header(matrix)
     data = np.random.randn(2, 10)
     img = ci.Cifti2Image(data, hdr)
-    img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_DENSE_LABELS')
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.dlabel.nii')
+        ci.save(img, 'test.dlabel.nii', infer_intent=True)
         img2 = nib.load('test.dlabel.nii')
         assert img2.nifti_header.get_intent()[0] == 'ConnDenseLabel'
         assert isinstance(img2, ci.Cifti2Image)
@@ -301,10 +299,9 @@ def test_dconn():
     hdr = ci.Cifti2Header(matrix)
     data = np.random.randn(10, 10)
     img = ci.Cifti2Image(data, hdr)
-    img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_DENSE')
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.dconn.nii')
+        ci.save(img, 'test.dconn.nii', infer_intent=True)
         img2 = nib.load('test.dconn.nii')
         assert img2.nifti_header.get_intent()[0] == 'ConnDense'
         assert isinstance(img2, ci.Cifti2Image)
@@ -323,10 +320,9 @@ def test_ptseries():
     hdr = ci.Cifti2Header(matrix)
     data = np.random.randn(13, 4)
     img = ci.Cifti2Image(data, hdr)
-    img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SERIES')
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.ptseries.nii')
+        ci.save(img, 'test.ptseries.nii', infer_intent=True)
         img2 = nib.load('test.ptseries.nii')
         assert img2.nifti_header.get_intent()[0] == 'ConnParcelSries'
         assert isinstance(img2, ci.Cifti2Image)
@@ -345,10 +341,9 @@ def test_pscalar():
     hdr = ci.Cifti2Header(matrix)
     data = np.random.randn(2, 4)
     img = ci.Cifti2Image(data, hdr)
-    img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SCALAR')
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.pscalar.nii')
+        ci.save(img, 'test.pscalar.nii', infer_intent=True)
         img2 = nib.load('test.pscalar.nii')
         assert img2.nifti_header.get_intent()[0] == 'ConnParcelScalr'
         assert isinstance(img2, ci.Cifti2Image)
@@ -367,10 +362,9 @@ def test_pdconn():
     hdr = ci.Cifti2Header(matrix)
     data = np.random.randn(10, 4)
     img = ci.Cifti2Image(data, hdr)
-    img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_PARCELLATED_DENSE')
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.pdconn.nii')
+        ci.save(img, 'test.pdconn.nii', infer_intent=True)
         img2 = ci.load('test.pdconn.nii')
         assert img2.nifti_header.get_intent()[0] == 'ConnParcelDense'
         assert isinstance(img2, ci.Cifti2Image)
@@ -389,10 +383,9 @@ def test_dpconn():
     hdr = ci.Cifti2Header(matrix)
     data = np.random.randn(4, 10)
     img = ci.Cifti2Image(data, hdr)
-    img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_DENSE_PARCELLATED')
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.dpconn.nii')
+        ci.save(img, 'test.dpconn.nii', infer_intent=True)
         img2 = ci.load('test.dpconn.nii')
         assert img2.nifti_header.get_intent()[0] == 'ConnDenseParcel'
         assert isinstance(img2, ci.Cifti2Image)
@@ -430,10 +423,9 @@ def test_pconn():
     hdr = ci.Cifti2Header(matrix)
     data = np.random.randn(4, 4)
     img = ci.Cifti2Image(data, hdr)
-    img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_PARCELLATED')
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.pconn.nii')
+        ci.save(img, 'test.pconn.nii', infer_intent=True)
         img2 = ci.load('test.pconn.nii')
         assert img.nifti_header.get_intent()[0] == 'ConnParcels'
         assert isinstance(img2, ci.Cifti2Image)
@@ -453,11 +445,9 @@ def test_pconnseries():
     hdr = ci.Cifti2Header(matrix)
     data = np.random.randn(4, 4, 13)
     img = ci.Cifti2Image(data, hdr)
-    img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_PARCELLATED_'
-                                'PARCELLATED_SERIES')
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.pconnseries.nii')
+        ci.save(img, 'test.pconnseries.nii', infer_intent=True)
         img2 = ci.load('test.pconnseries.nii')
         assert img.nifti_header.get_intent()[0] == 'ConnPPSr'
         assert isinstance(img2, ci.Cifti2Image)
@@ -478,11 +468,9 @@ def test_pconnscalar():
     hdr = ci.Cifti2Header(matrix)
     data = np.random.randn(4, 4, 2)
     img = ci.Cifti2Image(data, hdr)
-    img.nifti_header.set_intent('NIFTI_INTENT_CONNECTIVITY_PARCELLATED_'
-                                'PARCELLATED_SCALAR')
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.pconnscalar.nii')
+        ci.save(img, 'test.pconnscalar.nii', infer_intent=True)
         img2 = ci.load('test.pconnscalar.nii')
         assert img.nifti_header.get_intent()[0] == 'ConnPPSc'
         assert isinstance(img2, ci.Cifti2Image)
@@ -517,7 +505,7 @@ def test_wrong_shape():
                     ci.Cifti2Image(data, hdr)
         with suppress_warnings():
             img = ci.Cifti2Image(data, hdr)
-        
+
         with pytest.raises(ValueError):
             img.to_file_map()
 

From 2b6b328173d52b8909c005bfba64ea60330ce4c1 Mon Sep 17 00:00:00 2001
From: mathiasg <mathiasg@stanford.edu>
Date: Fri, 2 Oct 2020 16:25:58 -0400
Subject: [PATCH 3/9] ENH: Add validation when saving CIFTI2 images

- Enabled by default, validation will parse the output filename for a valid CIFTI2 extension.
- If found, the intent code of the image will be set. Also, the CIFTI2Header will be check for compliant index maps for the intent code
---
 nibabel/cifti2/cifti2.py                | 107 +++++++++++++++++-------
 nibabel/cifti2/tests/test_new_cifti2.py |  62 +++++++++++---
 2 files changed, 131 insertions(+), 38 deletions(-)

diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py
index 2cc6931de7..a6ec086718 100644
--- a/nibabel/cifti2/cifti2.py
+++ b/nibabel/cifti2/cifti2.py
@@ -25,6 +25,7 @@
 from ..nifti1 import Nifti1Extensions
 from ..nifti2 import Nifti2Image, Nifti2Header
 from ..arrayproxy import reshape_dataobj
+from ..volumeutils import Recoder
 from warnings import warn
 
 
@@ -91,20 +92,50 @@ class Cifti2HeaderError(Exception):
 
 # "Standard CIFTI Mapping Combinations" within CIFTI-2 spec
 # https://www.nitrc.org/forum/attachment.php?attachid=341&group_id=454&forum_id=1955
-CIFTI_EXTENSIONS_TO_INTENTS = {
-    '.dconn': 'NIFTI_INTENT_CONNECTIVITY_DENSE',
-    '.dtseries': 'NIFTI_INTENT_CONNECTIVITY_DENSE_SERIES',
-    '.pconn': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED',
-    '.ptseries': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SERIES',
-    '.dscalar': 'NIFTI_INTENT_CONNECTIVITY_DENSE_SCALARS',
-    '.dlabel': 'NIFTI_INTENT_CONNECTIVITY_DENSE_LABELS',
-    '.pscalar': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SCALAR',
-    '.pdconn': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_DENSE',
-    '.dpconn': 'NIFTI_INTENT_CONNECTIVITY_DENSE_PARCELLATED',
-    '.pconnseries': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_PARCELLATED_SERIES',
-    '.pconnscalar': 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_PARCELLATED_SCALAR',
-    '.dfan': 'NIFTI_INTENT_CONNECTIVITY_DENSE_SERIES',
-}
+CIFTI_CODES = Recoder((
+    ('dconn', 'NIFTI_INTENT_CONNECTIVITY_DENSE', (
+        'CIFTI_INDEX_TYPE_BRAIN_MODELS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
+    )),
+    ('dtseries', 'NIFTI_INTENT_CONNECTIVITY_DENSE_SERIES', (
+        'CIFTI_INDEX_TYPE_SERIES', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
+    )),
+    ('pconn', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED', (
+        'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_PARCELS',
+    )),
+    ('ptseries', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SERIES', (
+        'CIFTI_INDEX_TYPE_SERIES', 'CIFTI_INDEX_TYPE_PARCELS',
+    )),
+    ('dscalar', 'NIFTI_INTENT_CONNECTIVITY_DENSE_SCALARS', (
+        'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
+    )),
+    ('dlabel', 'NIFTI_INTENT_CONNECTIVITY_DENSE_LABELS', (
+        'CIFTI_INDEX_TYPE_LABELS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
+    )),
+    ('pscalar', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SCALAR', (
+        'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_PARCELS',
+    )),
+    ('pdconn', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_DENSE', (
+        'CIFTI_INDEX_TYPE_BRAIN_MODELS', 'CIFTI_INDEX_TYPE_PARCELS',
+    )),
+    ('dpconn', 'NIFTI_INTENT_CONNECTIVITY_DENSE_PARCELLATED', (
+        'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
+    )),
+    ('pconnseries', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_PARCELLATED_SERIES', (
+        'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_SERIES',
+    )),
+    ('pconnscalar', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_PARCELLATED_SCALAR', (
+        'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_SCALARS',
+    )),
+    ('dfan', 'NIFTI_INTENT_CONNECTIVITY_DENSE_SERIES', (
+        'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
+    )),
+    ('dfibersamp', 'NIFTI_INTENT_CONNECTIVITY_UNKNOWN', (
+        'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
+    )),
+    ('dfansamp', 'NIFTI_INTENT_CONNECTIVITY_UNKNOWN', (
+        'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
+    )),
+), fields=('extension', 'niistring', 'map_types'))
 
 
 def _value_if_klass(val, klass):
@@ -1522,32 +1553,52 @@ def get_data_dtype(self):
     def set_data_dtype(self, dtype):
         self._nifti_header.set_data_dtype(dtype)
 
-    def to_filename(self, filename, infer_intent=False):
+    def to_filename(self, filename, validate=True):
         """
         Ensures NIfTI header intent code is set prior to saving.
 
         Parameters
         ----------
-        infer_intent : boolean, optional
-            If ``True``, attempt to infer and set intent code based on filename suffix.
+        validate : boolean, optional
+            If ``True``, infer and validate CIFTI type based on filename suffix.
+            This includes the setting of the NIfTI intent code and checking the ``CIFTI2Matrix``
+            for the expected IndicesMaps attributes.
+            If validation fails, an error will be raised instead.
         """
-        header = self._nifti_header
-        if infer_intent:
-            # try to infer intent code based on filename suffix
-            intent = _infer_intent_from_filename(filename)
-            if intent is not None:
-                header.set_intent(intent)
+        nheader = self._nifti_header
+        # try to infer intent code based on filename suffix
+        if validate:
+            ext = _extract_cifti_extension(filename)
+            try:
+                CIFTI_CODES.extension[ext]
+            except KeyError as err:
+                raise KeyError(
+                    f"Validation failed: No information for extension {ext} available"
+                ) from err
+            intent = CIFTI_CODES.niistring[ext]
+            nheader.set_intent(intent)
+            # validate matrix indices
+            for idx, mtype in enumerate(CIFTI_CODES.map_types[ext]):
+                try:
+                    assert self.header.matrix.get_index_map(idx).indices_map_to_data_type == mtype
+                except Exception:
+                    raise Cifti2HeaderError(
+                        f"Validation failed: Cifti2Matrix index map {idx} does "
+                        f"not match expected type {mtype}"
+                    )
         # if intent code is not set, default to unknown
-        if header.get_intent()[0] == 'none':
-            header.set_intent('NIFTI_INTENT_CONNECTIVITY_UNKNOWN')
+        if nheader.get_intent()[0] == 'none':
+            nheader.set_intent('NIFTI_INTENT_CONNECTIVITY_UNKNOWN')
         super().to_filename(filename)
 
 
-def _infer_intent_from_filename(filename):
+def _extract_cifti_extension(filename):
     """Parses output filename for common suffixes and fetches corresponding intent code"""
     from pathlib import Path
-    ext = Path(filename).suffixes[0]
-    return CIFTI_EXTENSIONS_TO_INTENTS.get(ext)
+    _suf = Path(filename).suffixes
+    # select second to last if possible (.<suffix>.nii)
+    ext = _suf[-2] if len(_suf) >= 2 else _suf[0]
+    return ext.lstrip('.')
 
 
 load = Cifti2Image.from_filename
diff --git a/nibabel/cifti2/tests/test_new_cifti2.py b/nibabel/cifti2/tests/test_new_cifti2.py
index 5ee6f4a7c3..948e1aca41 100644
--- a/nibabel/cifti2/tests/test_new_cifti2.py
+++ b/nibabel/cifti2/tests/test_new_cifti2.py
@@ -239,7 +239,7 @@ def test_dtseries():
     img = ci.Cifti2Image(data, hdr)
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.dtseries.nii', infer_intent=True)
+        ci.save(img, 'test.dtseries.nii')
         img2 = nib.load('test.dtseries.nii')
         assert img2.nifti_header.get_intent()[0] == 'ConnDenseSeries'
         assert isinstance(img2, ci.Cifti2Image)
@@ -282,7 +282,7 @@ def test_dlabel():
     img = ci.Cifti2Image(data, hdr)
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.dlabel.nii', infer_intent=True)
+        ci.save(img, 'test.dlabel.nii')
         img2 = nib.load('test.dlabel.nii')
         assert img2.nifti_header.get_intent()[0] == 'ConnDenseLabel'
         assert isinstance(img2, ci.Cifti2Image)
@@ -301,7 +301,7 @@ def test_dconn():
     img = ci.Cifti2Image(data, hdr)
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.dconn.nii', infer_intent=True)
+        ci.save(img, 'test.dconn.nii')
         img2 = nib.load('test.dconn.nii')
         assert img2.nifti_header.get_intent()[0] == 'ConnDense'
         assert isinstance(img2, ci.Cifti2Image)
@@ -322,7 +322,7 @@ def test_ptseries():
     img = ci.Cifti2Image(data, hdr)
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.ptseries.nii', infer_intent=True)
+        ci.save(img, 'test.ptseries.nii')
         img2 = nib.load('test.ptseries.nii')
         assert img2.nifti_header.get_intent()[0] == 'ConnParcelSries'
         assert isinstance(img2, ci.Cifti2Image)
@@ -343,7 +343,7 @@ def test_pscalar():
     img = ci.Cifti2Image(data, hdr)
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.pscalar.nii', infer_intent=True)
+        ci.save(img, 'test.pscalar.nii')
         img2 = nib.load('test.pscalar.nii')
         assert img2.nifti_header.get_intent()[0] == 'ConnParcelScalr'
         assert isinstance(img2, ci.Cifti2Image)
@@ -364,7 +364,7 @@ def test_pdconn():
     img = ci.Cifti2Image(data, hdr)
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.pdconn.nii', infer_intent=True)
+        ci.save(img, 'test.pdconn.nii')
         img2 = ci.load('test.pdconn.nii')
         assert img2.nifti_header.get_intent()[0] == 'ConnParcelDense'
         assert isinstance(img2, ci.Cifti2Image)
@@ -385,7 +385,7 @@ def test_dpconn():
     img = ci.Cifti2Image(data, hdr)
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.dpconn.nii', infer_intent=True)
+        ci.save(img, 'test.dpconn.nii')
         img2 = ci.load('test.dpconn.nii')
         assert img2.nifti_header.get_intent()[0] == 'ConnDenseParcel'
         assert isinstance(img2, ci.Cifti2Image)
@@ -425,7 +425,7 @@ def test_pconn():
     img = ci.Cifti2Image(data, hdr)
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.pconn.nii', infer_intent=True)
+        ci.save(img, 'test.pconn.nii')
         img2 = ci.load('test.pconn.nii')
         assert img.nifti_header.get_intent()[0] == 'ConnParcels'
         assert isinstance(img2, ci.Cifti2Image)
@@ -447,7 +447,7 @@ def test_pconnseries():
     img = ci.Cifti2Image(data, hdr)
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.pconnseries.nii', infer_intent=True)
+        ci.save(img, 'test.pconnseries.nii')
         img2 = ci.load('test.pconnseries.nii')
         assert img.nifti_header.get_intent()[0] == 'ConnPPSr'
         assert isinstance(img2, ci.Cifti2Image)
@@ -470,7 +470,7 @@ def test_pconnscalar():
     img = ci.Cifti2Image(data, hdr)
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.pconnscalar.nii', infer_intent=True)
+        ci.save(img, 'test.pconnscalar.nii')
         img2 = ci.load('test.pconnscalar.nii')
         assert img.nifti_header.get_intent()[0] == 'ConnPPSc'
         assert isinstance(img2, ci.Cifti2Image)
@@ -509,3 +509,45 @@ def test_wrong_shape():
         with pytest.raises(ValueError):
             img.to_file_map()
 
+
+def test_cifti_validation():
+    # flip label / brain_model index maps
+    geometry_map = create_geometry_map((0, ))
+    label_map = create_label_map((1, ))
+    matrix = ci.Cifti2Matrix()
+    matrix.append(label_map)
+    matrix.append(geometry_map)
+    hdr = ci.Cifti2Header(matrix)
+    data = np.random.randn(10, 2)
+    img = ci.Cifti2Image(data, hdr)
+
+    # attempt to save and validate with an invalid extension
+    with pytest.raises(KeyError):
+        ci.save(img, 'test.dlabelz.nii')
+    # even with a proper extension, flipped index maps will fail
+    with pytest.raises(ci.Cifti2HeaderError):
+        ci.save(img, 'test.dlabel.nii')
+
+    label_map = create_label_map((0, ))
+    geometry_map = create_geometry_map((1, ))
+    matrix = ci.Cifti2Matrix()
+    matrix.append(label_map)
+    matrix.append(geometry_map)
+    hdr = ci.Cifti2Header(matrix)
+    data = np.random.randn(2, 10)
+    img = ci.Cifti2Image(data, hdr)
+
+    with InTemporaryDirectory():
+        # still fail with invalid extension and validation
+        with pytest.raises(KeyError):
+            ci.save(img, 'test.dlabelz.nii')
+        # but removing validation should work (though intent code will be unknown)
+        ci.save(img, 'test.dlabelz.nii', validate=False)
+
+        img2 = nib.load('test.dlabelz.nii')
+        assert img2.nifti_header.get_intent()[0] == 'ConnUnknown'
+        assert isinstance(img2, ci.Cifti2Image)
+        assert_array_equal(img2.get_fdata(), data)
+        check_label_map(img2.header.matrix.get_index_map(0))
+        check_geometry_map(img2.header.matrix.get_index_map(1))
+        del img2

From ad6adfa5b30e317939990962f188aaf72aca8161 Mon Sep 17 00:00:00 2001
From: mathiasg <mathiasg@stanford.edu>
Date: Fri, 2 Oct 2020 18:57:59 -0400
Subject: [PATCH 4/9] RF: Avoid validation when testing CIFTI saving

---
 nibabel/cifti2/tests/test_cifti2.py        | 15 +++++++++++++++
 nibabel/cifti2/tests/test_cifti2io_axes.py |  2 +-
 nibabel/cifti2/tests/test_new_cifti2.py    |  2 +-
 nibabel/tests/test_image_api.py            | 20 ++++++++++----------
 4 files changed, 27 insertions(+), 12 deletions(-)

diff --git a/nibabel/cifti2/tests/test_cifti2.py b/nibabel/cifti2/tests/test_cifti2.py
index ea571065de..f64e11c5df 100644
--- a/nibabel/cifti2/tests/test_cifti2.py
+++ b/nibabel/cifti2/tests/test_cifti2.py
@@ -427,3 +427,18 @@ def make_imaker(self, arr, header=None, ni_header=None):
             )
             header.matrix.append(mim)
         return lambda: self.image_maker(arr.copy(), header, ni_header)
+
+    def validate_filenames(self, imaker, params, validate=False):
+        super().validate_filenames(imaker, params, validate=validate)
+
+    def validate_mmap_parameter(self, imaker, params, validate=False):
+        super().validate_mmap_parameter(imaker, params, validate=validate)
+
+    def validate_to_bytes(self, imaker, params, validate=False):
+        super().validate_to_bytes(imaker, params, validate=validate)
+
+    def validate_from_bytes(self, imaker, params, validate=False):
+        super().validate_from_bytes(imaker, params, validate=validate)
+
+    def validate_to_from_bytes(self, imaker, params, validate=False):
+        super().validate_to_from_bytes(imaker, params, validate=validate)
diff --git a/nibabel/cifti2/tests/test_cifti2io_axes.py b/nibabel/cifti2/tests/test_cifti2io_axes.py
index c237e3c61a..1abd129ae5 100644
--- a/nibabel/cifti2/tests/test_cifti2io_axes.py
+++ b/nibabel/cifti2/tests/test_cifti2io_axes.py
@@ -91,7 +91,7 @@ def check_rewrite(arr, axes, extension='.nii'):
         custom extension to use
     """
     (fd, name) = tempfile.mkstemp(extension)
-    cifti2.Cifti2Image(arr, header=axes).to_filename(name)
+    cifti2.Cifti2Image(arr, header=axes).to_filename(name, validate=False)
     img = nib.load(name)
     arr2 = img.get_fdata()
     assert np.allclose(arr, arr2)
diff --git a/nibabel/cifti2/tests/test_new_cifti2.py b/nibabel/cifti2/tests/test_new_cifti2.py
index 948e1aca41..2bc6da3b47 100644
--- a/nibabel/cifti2/tests/test_new_cifti2.py
+++ b/nibabel/cifti2/tests/test_new_cifti2.py
@@ -406,7 +406,7 @@ def test_plabel():
     img = ci.Cifti2Image(data, hdr)
 
     with InTemporaryDirectory():
-        ci.save(img, 'test.plabel.nii')
+        ci.save(img, 'test.plabel.nii', validate=False)
         img2 = ci.load('test.plabel.nii')
         assert img.nifti_header.get_intent()[0] == 'ConnUnknown'
         assert isinstance(img2, ci.Cifti2Image)
diff --git a/nibabel/tests/test_image_api.py b/nibabel/tests/test_image_api.py
index 392a493e53..9a9ae040c4 100644
--- a/nibabel/tests/test_image_api.py
+++ b/nibabel/tests/test_image_api.py
@@ -123,7 +123,7 @@ def validate_header_deprecated(self, imaker, params):
             hdr = img.get_header()
         assert hdr is img.header
 
-    def validate_filenames(self, imaker, params):
+    def validate_filenames(self, imaker, params, **kwargs):
         # Validate the filename, file_map interface
 
         if not self.can_save:
@@ -160,7 +160,7 @@ def validate_filenames(self, imaker, params):
                   warnings.filterwarnings('error',
                                           category=DeprecationWarning,
                                           module=r"nibabel.*")
-                  img.to_filename(path)
+                  img.to_filename(path, **kwargs)
                   rt_img = img.__class__.from_filename(path)
               assert_array_equal(img.shape, rt_img.shape)
               assert_almost_equal(img.get_fdata(), rt_img.get_fdata())
@@ -456,7 +456,7 @@ def validate_shape_deprecated(self, imaker, params):
         with pytest.raises(ExpiredDeprecationError):
             img.get_shape()
 
-    def validate_mmap_parameter(self, imaker, params):
+    def validate_mmap_parameter(self, imaker, params, **kwargs):
         img = imaker()
         fname = img.get_filename()
         with InTemporaryDirectory():
@@ -468,7 +468,7 @@ def validate_mmap_parameter(self, imaker, params):
                 if not img.rw or not img.valid_exts:
                     return
                 fname = 'image' + img.valid_exts[0]
-                img.to_filename(fname)
+                img.to_filename(fname, **kwargs)
             rt_img = img.__class__.from_filename(fname, mmap=True)
             assert_almost_equal(img.get_fdata(), rt_img.get_fdata())
             rt_img = img.__class__.from_filename(fname, mmap=False)
@@ -533,22 +533,22 @@ def validate_affine_deprecated(self, imaker, params):
 
 
 class SerializeMixin(object):
-    def validate_to_bytes(self, imaker, params):
+    def validate_to_bytes(self, imaker, params, **kwargs):
         img = imaker()
         serialized = img.to_bytes()
         with InTemporaryDirectory():
             fname = 'img' + self.standard_extension
-            img.to_filename(fname)
+            img.to_filename(fname, **kwargs)
             with open(fname, 'rb') as fobj:
                 file_contents = fobj.read()
         assert serialized == file_contents
 
-    def validate_from_bytes(self, imaker, params):
+    def validate_from_bytes(self, imaker, params, **kwargs):
         img = imaker()
         klass = getattr(self, 'klass', img.__class__)
         with InTemporaryDirectory():
             fname = 'img' + self.standard_extension
-            img.to_filename(fname)
+            img.to_filename(fname, **kwargs)
 
             all_images = list(getattr(self, 'example_images', [])) + [{'fname': fname}]
             for img_params in all_images:
@@ -561,12 +561,12 @@ def validate_from_bytes(self, imaker, params):
                 del img_a
                 del img_b
 
-    def validate_to_from_bytes(self, imaker, params):
+    def validate_to_from_bytes(self, imaker, params, **kwargs):
         img = imaker()
         klass = getattr(self, 'klass', img.__class__)
         with InTemporaryDirectory():
             fname = 'img' + self.standard_extension
-            img.to_filename(fname)
+            img.to_filename(fname, **kwargs)
 
             all_images = list(getattr(self, 'example_images', [])) + [{'fname': fname}]
             for img_params in all_images:

From 2a20433db3f58c5fc30bc316c066fec661be0167 Mon Sep 17 00:00:00 2001
From: mathiasg <mathiasg@stanford.edu>
Date: Mon, 5 Oct 2020 11:05:49 -0400
Subject: [PATCH 5/9] RF: Extract nifti header update from ``to_filename``

---
 nibabel/cifti2/cifti2.py | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py
index a6ec086718..98e4a83a89 100644
--- a/nibabel/cifti2/cifti2.py
+++ b/nibabel/cifti2/cifti2.py
@@ -1545,7 +1545,11 @@ def update_headers(self):
         >>> img.shape == (2, 3, 4)
         True
         """
-        self._nifti_header.set_data_shape((1, 1, 1, 1) + self._dataobj.shape)
+        header = self._nifti_header
+        header.set_data_shape((1, 1, 1, 1) + self._dataobj.shape)
+        # if intent code is not set, default to unknown
+        if header.get_intent()[0] == 'none':
+            header.set_intent('NIFTI_INTENT_CONNECTIVITY_UNKNOWN')
 
     def get_data_dtype(self):
         return self._nifti_header.get_data_dtype()
@@ -1565,8 +1569,6 @@ def to_filename(self, filename, validate=True):
             for the expected IndicesMaps attributes.
             If validation fails, an error will be raised instead.
         """
-        nheader = self._nifti_header
-        # try to infer intent code based on filename suffix
         if validate:
             ext = _extract_cifti_extension(filename)
             try:
@@ -1576,7 +1578,7 @@ def to_filename(self, filename, validate=True):
                     f"Validation failed: No information for extension {ext} available"
                 ) from err
             intent = CIFTI_CODES.niistring[ext]
-            nheader.set_intent(intent)
+            self._nifti_header.set_intent(intent)
             # validate matrix indices
             for idx, mtype in enumerate(CIFTI_CODES.map_types[ext]):
                 try:
@@ -1586,9 +1588,6 @@ def to_filename(self, filename, validate=True):
                         f"Validation failed: Cifti2Matrix index map {idx} does "
                         f"not match expected type {mtype}"
                     )
-        # if intent code is not set, default to unknown
-        if nheader.get_intent()[0] == 'none':
-            nheader.set_intent('NIFTI_INTENT_CONNECTIVITY_UNKNOWN')
         super().to_filename(filename)
 
 

From 619295e9c6990f0ce6cbeb08f266a562b9da8d86 Mon Sep 17 00:00:00 2001
From: mathiasg <mathiasg@stanford.edu>
Date: Mon, 5 Oct 2020 11:37:23 -0400
Subject: [PATCH 6/9] FIX: remove validation for remaining tests

---
 nibabel/cifti2/tests/test_cifti2io_header.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/nibabel/cifti2/tests/test_cifti2io_header.py b/nibabel/cifti2/tests/test_cifti2io_header.py
index df4fe10fcd..f1f1f6c811 100644
--- a/nibabel/cifti2/tests/test_cifti2io_header.py
+++ b/nibabel/cifti2/tests/test_cifti2io_header.py
@@ -83,7 +83,7 @@ def test_readwritedata():
     with InTemporaryDirectory():
         for name in datafiles:
             img = ci.load(name)
-            ci.save(img, 'test.nii')
+            ci.save(img, 'test.nii', validate=False)
             img2 = ci.load('test.nii')
             assert len(img.header.matrix) == len(img2.header.matrix)
             # Order should be preserved in load/save
@@ -109,7 +109,7 @@ def test_nibabel_readwritedata():
     with InTemporaryDirectory():
         for name in datafiles:
             img = nib.load(name)
-            nib.save(img, 'test.nii')
+            nib.save(img, 'test.nii', validate=False)
             img2 = nib.load('test.nii')
             assert len(img.header.matrix) == len(img2.header.matrix)
             # Order should be preserved in load/save

From 62be507ccd01f3568dd52ea3d0649440e1a75913 Mon Sep 17 00:00:00 2001
From: mathiasg <mathiasg@stanford.edu>
Date: Mon, 5 Oct 2020 12:53:25 -0400
Subject: [PATCH 7/9] ENH: Allow kwargs in generic save method

---
 nibabel/loadsave.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/nibabel/loadsave.py b/nibabel/loadsave.py
index f3a2b5876b..eb112b158c 100644
--- a/nibabel/loadsave.py
+++ b/nibabel/loadsave.py
@@ -78,7 +78,7 @@ def guessed_image_type(filename):
     raise ImageFileError(f'Cannot work out file type of "{filename}"')
 
 
-def save(img, filename):
+def save(img, filename, **kwargs):
     """ Save an image to file adapting format to `filename`
 
     Parameters
@@ -96,7 +96,7 @@ def save(img, filename):
 
     # Save the type as expected
     try:
-        img.to_filename(filename)
+        img.to_filename(filename, **kwargs)
     except ImageFileError:
         pass
     else:
@@ -144,7 +144,7 @@ def save(img, filename):
     # Here, we either have a klass or a converted image.
     if converted is None:
         converted = klass.from_image(img)
-    converted.to_filename(filename)
+    converted.to_filename(filename, **kwargs)
 
 
 @deprecate_with_version('read_img_data deprecated. '

From 03ef43d65f497d2a16facc0262d3b823e677ab24 Mon Sep 17 00:00:00 2001
From: mathiasg <mathiasg@stanford.edu>
Date: Mon, 19 Oct 2020 13:33:45 -0400
Subject: [PATCH 8/9] FIX: Lower severity of failed validation checks

---
 nibabel/cifti2/cifti2.py | 87 ++++++++++++++++++++--------------------
 1 file changed, 43 insertions(+), 44 deletions(-)

diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py
index 98e4a83a89..a0cd22fa58 100644
--- a/nibabel/cifti2/cifti2.py
+++ b/nibabel/cifti2/cifti2.py
@@ -93,46 +93,46 @@ class Cifti2HeaderError(Exception):
 # "Standard CIFTI Mapping Combinations" within CIFTI-2 spec
 # https://www.nitrc.org/forum/attachment.php?attachid=341&group_id=454&forum_id=1955
 CIFTI_CODES = Recoder((
-    ('dconn', 'NIFTI_INTENT_CONNECTIVITY_DENSE', (
+    ('.dconn.nii', 'NIFTI_INTENT_CONNECTIVITY_DENSE', (
         'CIFTI_INDEX_TYPE_BRAIN_MODELS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
     )),
-    ('dtseries', 'NIFTI_INTENT_CONNECTIVITY_DENSE_SERIES', (
+    ('.dtseries.nii', 'NIFTI_INTENT_CONNECTIVITY_DENSE_SERIES', (
         'CIFTI_INDEX_TYPE_SERIES', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
     )),
-    ('pconn', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED', (
+    ('.pconn.nii', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED', (
         'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_PARCELS',
     )),
-    ('ptseries', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SERIES', (
+    ('.ptseries.nii', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SERIES', (
         'CIFTI_INDEX_TYPE_SERIES', 'CIFTI_INDEX_TYPE_PARCELS',
     )),
-    ('dscalar', 'NIFTI_INTENT_CONNECTIVITY_DENSE_SCALARS', (
+    ('.dscalar.nii', 'NIFTI_INTENT_CONNECTIVITY_DENSE_SCALARS', (
         'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
     )),
-    ('dlabel', 'NIFTI_INTENT_CONNECTIVITY_DENSE_LABELS', (
+    ('.dlabel.nii', 'NIFTI_INTENT_CONNECTIVITY_DENSE_LABELS', (
         'CIFTI_INDEX_TYPE_LABELS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
     )),
-    ('pscalar', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SCALAR', (
+    ('.pscalar.nii', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_SCALAR', (
         'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_PARCELS',
     )),
-    ('pdconn', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_DENSE', (
+    ('.pdconn.nii', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_DENSE', (
         'CIFTI_INDEX_TYPE_BRAIN_MODELS', 'CIFTI_INDEX_TYPE_PARCELS',
     )),
-    ('dpconn', 'NIFTI_INTENT_CONNECTIVITY_DENSE_PARCELLATED', (
+    ('.dpconn.nii', 'NIFTI_INTENT_CONNECTIVITY_DENSE_PARCELLATED', (
         'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
     )),
-    ('pconnseries', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_PARCELLATED_SERIES', (
+    ('.pconnseries.nii', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_PARCELLATED_SERIES', (
         'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_SERIES',
     )),
-    ('pconnscalar', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_PARCELLATED_SCALAR', (
+    ('.pconnscalar.nii', 'NIFTI_INTENT_CONNECTIVITY_PARCELLATED_PARCELLATED_SCALAR', (
         'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_PARCELS', 'CIFTI_INDEX_TYPE_SCALARS',
     )),
-    ('dfan', 'NIFTI_INTENT_CONNECTIVITY_DENSE_SERIES', (
+    ('.dfan.nii', 'NIFTI_INTENT_CONNECTIVITY_DENSE_SERIES', (
         'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
     )),
-    ('dfibersamp', 'NIFTI_INTENT_CONNECTIVITY_UNKNOWN', (
+    ('.dfibersamp.nii', 'NIFTI_INTENT_CONNECTIVITY_UNKNOWN', (
         'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
     )),
-    ('dfansamp', 'NIFTI_INTENT_CONNECTIVITY_UNKNOWN', (
+    ('.dfansamp.nii', 'NIFTI_INTENT_CONNECTIVITY_UNKNOWN', (
         'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_SCALARS', 'CIFTI_INDEX_TYPE_BRAIN_MODELS',
     )),
 ), fields=('extension', 'niistring', 'map_types'))
@@ -1564,40 +1564,39 @@ def to_filename(self, filename, validate=True):
         Parameters
         ----------
         validate : boolean, optional
-            If ``True``, infer and validate CIFTI type based on filename suffix.
-            This includes the setting of the NIfTI intent code and checking the ``CIFTI2Matrix``
-            for the expected IndicesMaps attributes.
-            If validation fails, an error will be raised instead.
+            If ``True``, infer and validate CIFTI type based on MatrixIndicesMap values.
+            This includes the setting of the relevant intent code within the NIfTI header.
+            If validation fails, a UserWarning is issued and saving continues.
         """
         if validate:
-            ext = _extract_cifti_extension(filename)
-            try:
-                CIFTI_CODES.extension[ext]
-            except KeyError as err:
-                raise KeyError(
-                    f"Validation failed: No information for extension {ext} available"
-                ) from err
-            intent = CIFTI_CODES.niistring[ext]
-            self._nifti_header.set_intent(intent)
-            # validate matrix indices
-            for idx, mtype in enumerate(CIFTI_CODES.map_types[ext]):
-                try:
-                    assert self.header.matrix.get_index_map(idx).indices_map_to_data_type == mtype
-                except Exception:
-                    raise Cifti2HeaderError(
-                        f"Validation failed: Cifti2Matrix index map {idx} does "
-                        f"not match expected type {mtype}"
-                    )
-        super().to_filename(filename)
+            # Determine CIFTI type via index maps
+            from .parse_cifti2 import intent_codes
 
+            matrix = self.header.matrix
+            map_types = tuple(
+                matrix.get_index_map(idx).indices_map_to_data_type for idx
+                in sorted(matrix.mapped_indices)
+            )
+            try:
+                expected_intent = CIFTI_CODES.niistring[map_types]
+                expected_ext = CIFTI_CODES.extension[map_types]
+            except KeyError:  # unknown
+                expected_intent = "NIFTI_INTENT_CONNECTIVITY_UNKNOWN"
+                expected_ext = None
+                warn(
+                    "No information found for matrix containing the following index maps:"
+                    f"{map_types}, defaulting to unknown."
+                )
 
-def _extract_cifti_extension(filename):
-    """Parses output filename for common suffixes and fetches corresponding intent code"""
-    from pathlib import Path
-    _suf = Path(filename).suffixes
-    # select second to last if possible (.<suffix>.nii)
-    ext = _suf[-2] if len(_suf) >= 2 else _suf[0]
-    return ext.lstrip('.')
+            orig_intent = self._nifti_header.get_intent()[0]
+            if expected_intent != intent_codes.niistring[orig_intent]:
+                warn(
+                    f"Expected NIfTI intent: {expected_intent} has been automatically set."
+                )
+                self._nifti_header.set_intent(expected_intent)
+            if expected_ext is not None and not filename.endswith(expected_ext):
+                warn(f"Filename does not end with expected extension: {expected_ext}")
+        super().to_filename(filename)
 
 
 load = Cifti2Image.from_filename

From af7265ceee16cbdfa753355908eca8c0505bd988 Mon Sep 17 00:00:00 2001
From: mathiasg <mathiasg@stanford.edu>
Date: Mon, 19 Oct 2020 13:34:33 -0400
Subject: [PATCH 9/9] TST: Clean up test

---
 nibabel/cifti2/tests/test_new_cifti2.py | 31 +++++++++++--------------
 1 file changed, 13 insertions(+), 18 deletions(-)

diff --git a/nibabel/cifti2/tests/test_new_cifti2.py b/nibabel/cifti2/tests/test_new_cifti2.py
index 2bc6da3b47..026fc920f0 100644
--- a/nibabel/cifti2/tests/test_new_cifti2.py
+++ b/nibabel/cifti2/tests/test_new_cifti2.py
@@ -7,12 +7,11 @@
 scratch.
 """
 import numpy as np
-
 import nibabel as nib
 from nibabel import cifti2 as ci
 from nibabel.tmpdirs import InTemporaryDirectory
-
 import pytest
+
 from ...testing import (
     clear_and_catch_warnings, error_warnings, suppress_warnings, assert_array_equal)
 
@@ -515,17 +514,13 @@ def test_cifti_validation():
     geometry_map = create_geometry_map((0, ))
     label_map = create_label_map((1, ))
     matrix = ci.Cifti2Matrix()
-    matrix.append(label_map)
     matrix.append(geometry_map)
+    matrix.append(label_map)
     hdr = ci.Cifti2Header(matrix)
     data = np.random.randn(10, 2)
     img = ci.Cifti2Image(data, hdr)
-
-    # attempt to save and validate with an invalid extension
-    with pytest.raises(KeyError):
-        ci.save(img, 'test.dlabelz.nii')
-    # even with a proper extension, flipped index maps will fail
-    with pytest.raises(ci.Cifti2HeaderError):
+    # flipped index maps will warn
+    with InTemporaryDirectory(), pytest.warns(UserWarning):
         ci.save(img, 'test.dlabel.nii')
 
     label_map = create_label_map((0, ))
@@ -538,16 +533,16 @@ def test_cifti_validation():
     img = ci.Cifti2Image(data, hdr)
 
     with InTemporaryDirectory():
-        # still fail with invalid extension and validation
-        with pytest.raises(KeyError):
-            ci.save(img, 'test.dlabelz.nii')
-        # but removing validation should work (though intent code will be unknown)
-        ci.save(img, 'test.dlabelz.nii', validate=False)
-
-        img2 = nib.load('test.dlabelz.nii')
-        assert img2.nifti_header.get_intent()[0] == 'ConnUnknown'
+        ci.save(img, 'test.validate.nii', validate=False)
+        ci.save(img, 'test.dlabel.nii')
+
+        img2 = nib.load('test.dlabel.nii')
+        img3 = nib.load('test.validate.nii')
+        assert img2.nifti_header.get_intent()[0] == 'ConnDenseLabel'
+        assert img3.nifti_header.get_intent()[0] == 'ConnUnknown'
         assert isinstance(img2, ci.Cifti2Image)
+        assert isinstance(img3, ci.Cifti2Image)
         assert_array_equal(img2.get_fdata(), data)
         check_label_map(img2.header.matrix.get_index_map(0))
         check_geometry_map(img2.header.matrix.get_index_map(1))
-        del img2
+        del img2, img3
\ No newline at end of file