Repository: nilearn/nilearn

Issue:
Bug in _uncompress_file
Hi!
(...still nilearn-sprinting in the train back to Marseille ;) )

I found a bug when trying to download .tar.gz archive files from osf.

I got an error when using _fetch_files:

```
from nilearn.datasets.utils import _fetch_files
opts = {'uncompress': True}
data_dir = '~/nilearn_data'
url = 'https://osf.io/vym9k/download'
_fetch_files(data_dir, [('sphere_right.gii', url, opts)])
```

After digging, I realized the problem is in _uncompress_file: the temporary downloaded file is actually called 'download' on disk (because of the url that's given by osf), and  _uncompress_file does not extract the files if the archive was a .tar.gz one (it does not return an error, and it erases the 'download' file)

Note that:
- if the archive is a zip file, everything works... (as in the fetch_icbm152_2009 function); the temporary file is also called 'download' on disk, but _uncompress_file handles it properly
- if the file on disk is actually called 'whatever.tar.gz' and you run _uncompress_file on it, it also handles it properly

Soooooooooo, to reproduce the problem, the easiest is that:
1. you type https://osf.io/vym9k/download in your browser to get the file
2. you rename the file manually in your unix shell
```
> mv sphere_mesh.tar.gz download
```
3. you try to use _uncompress_file on it
```
from nilearn.datasets.utils import _uncompress_file
_uncompress_file('download')
```
It will: 1. not extract the files in the archive, 2. erase the 'download' file, 3. not return an error.

I hope this is clear enough...

Sylvain




Discussion:
@SylvainTakerkart  asked me to replace on OSF the two .tar.gz files with the .zip files and send the two new links.
On it.


InterTVA: https://osf.io/aekp7/download
sphere mesh: https://osf.io/b79fy/download
Thanks! I can then move forward with writing my fetchers (just created a PR for the sphere meshes)...

But this bug will still need to be looked at at some point...
~File an issue, with all the details.~

Edit: Sorry I thought this was a PR.

Resolving patch:
diff --git a/nilearn/image/image.py b/nilearn/image/image.py
--- a/nilearn/image/image.py
+++ b/nilearn/image/image.py
@@ -175,17 +175,19 @@ def _smooth_array(arr, affine, fwhm=None, ensure_finite=True, copy=True):
         are also accepted (only these coefficients are used).
         If fwhm='fast', the affine is not used and can be None
 
-    fwhm: scalar, numpy.ndarray, 'fast' or None
+    fwhm: scalar, numpy.ndarray/tuple/list, 'fast' or None
         Smoothing strength, as a full-width at half maximum, in millimeters.
-        If a scalar is given, width is identical on all three directions.
-        A numpy.ndarray must have 3 elements, giving the FWHM along each axis.
+        If a nonzero scalar is given, width is identical in all 3 directions.
+        A numpy.ndarray/list/tuple must have 3 elements,
+        giving the FWHM along each axis.
+        If any of the elements is zero or None,
+        smoothing is not performed along that axis.
         If fwhm == 'fast', a fast smoothing will be performed with
         a filter [0.2, 1, 0.2] in each direction and a normalisation
         to preserve the local average value.
         If fwhm is None, no filtering is performed (useful when just removal
         of non-finite values is needed).
 
-
     ensure_finite: bool
         if True, replace every non-finite values (like NaNs) by zero before
         filtering.
@@ -205,38 +207,34 @@ def _smooth_array(arr, affine, fwhm=None, ensure_finite=True, copy=True):
     """
     # Here, we have to investigate use cases of fwhm. Particularly, if fwhm=0.
     # See issue #1537
-    if fwhm == 0.:
+    if isinstance(fwhm, (int, float)) and (fwhm == 0.0):
         warnings.warn("The parameter 'fwhm' for smoothing is specified "
-                      "as {0}. Converting to None (no smoothing option)"
+                      "as {0}. Setting it to None "
+                      "(no smoothing will be performed)"
                       .format(fwhm))
         fwhm = None
-
     if arr.dtype.kind == 'i':
         if arr.dtype == np.int64:
             arr = arr.astype(np.float64)
         else:
-            # We don't need crazy precision
-            arr = arr.astype(np.float32)
+            arr = arr.astype(np.float32)  # We don't need crazy precision.
     if copy:
         arr = arr.copy()
-
     if ensure_finite:
         # SPM tends to put NaNs in the data outside the brain
         arr[np.logical_not(np.isfinite(arr))] = 0
-
-    if fwhm == 'fast':
+    if isinstance(fwhm, str) and (fwhm == 'fast'):
         arr = _fast_smooth_array(arr)
     elif fwhm is not None:
-        # Keep only the scale part.
-        affine = affine[:3, :3]
-
-        # Convert from a FWHM to a sigma:
-        fwhm_over_sigma_ratio = np.sqrt(8 * np.log(2))
+        fwhm = np.asarray(fwhm)
+        fwhm = np.where(fwhm == None, 0.0, fwhm)  # noqa: E711
+        affine = affine[:3, :3]  # Keep only the scale part.
+        fwhm_over_sigma_ratio = np.sqrt(8 * np.log(2))  # FWHM to sigma.
         vox_size = np.sqrt(np.sum(affine ** 2, axis=0))
         sigma = fwhm / (fwhm_over_sigma_ratio * vox_size)
         for n, s in enumerate(sigma):
-            ndimage.gaussian_filter1d(arr, s, output=arr, axis=n)
-
+            if s > 0.0:
+                ndimage.gaussian_filter1d(arr, s, output=arr, axis=n)
     return arr
 
 


Test patch:
diff --git a/nilearn/tests/test_masking.py b/nilearn/tests/test_masking.py
--- a/nilearn/tests/test_masking.py
+++ b/nilearn/tests/test_masking.py
@@ -4,6 +4,7 @@
 import distutils.version
 import warnings
 import numpy as np
+import pytest
 
 from numpy.testing import assert_array_equal
 from nose.tools import (
@@ -73,10 +74,8 @@ def test_compute_epi_mask():
     mean_image[0, 0, 0] = 1.2
     mean_image[0, 0, 2] = 1.1
     mean_image = Nifti1Image(mean_image, np.eye(4))
-    with warnings.catch_warnings(record=True) as w:
+    with pytest.warns(MaskWarning, match='Computed an empty mask'):
         compute_epi_mask(mean_image, exclude_zeros=True)
-    assert_equal(len(w), 1)
-    assert_true(isinstance(w[0].message, masking.MaskWarning))
 
 
 def test_compute_background_mask():