Repository: nilearn/nilearn

Issue:
Have the niimgs checking function optionally convert globbing
SHould we expand it? It would be more convenient for me but I don't know if it's YAGNI or not.



Discussion:
I think it's YAGNI, and we'll bring us more trouble than the anticipated good it might do for users. Let them expand their paths themselves. Also, `nilearn.plotting` is not supposed to be a unix shell environment ;)

I think that it might be useful, but let's tackle this later: I'd like to
have the niimgs checking function optionally convert globbing: the reason
is that when you have begining users, having to import glob actually
looses them.

Of course, we would need an option (with an API as a np.seterr, and a
context manager) to turn this off.

Can I rename the issue, and edit it's description to set this as a new goal?


Resolving patch:
diff --git a/nilearn/__init__.py b/nilearn/__init__.py
--- a/nilearn/__init__.py
+++ b/nilearn/__init__.py
@@ -47,7 +47,12 @@
 if hasattr(gzip.GzipFile, 'max_read_chunk'):
     gzip.GzipFile.max_read_chunk = 100 * 1024 * 1024  # 100Mb
 
-# Boolean controling whether the joblib caches should be
+# Boolean controlling the default globbing technique when using check_niimg
+# Default value it True, set it to False to completely deactivate use of glob
+# module
+EXPAND_PATH_WILDCARDS = True
+
+# Boolean controlling whether the joblib caches should be
 # flushed if the version of certain modules changes (eg nibabel, as it
 # does not respect the backward compatibility in some of its internal
 # structures
diff --git a/nilearn/_utils/niimg_conversions.py b/nilearn/_utils/niimg_conversions.py
--- a/nilearn/_utils/niimg_conversions.py
+++ b/nilearn/_utils/niimg_conversions.py
@@ -4,7 +4,10 @@
 # Author: Gael Varoquaux, Alexandre Abraham, Philippe Gervais
 # License: simplified BSD
 import warnings
+import os.path
+import glob
 
+import nilearn as ni
 import numpy as np
 import itertools
 from sklearn.externals.joblib import Memory
@@ -13,8 +16,7 @@
 from .niimg import _safe_get_data, load_niimg
 from .compat import _basestring, izip
 
-from nilearn._utils.exceptions import DimensionError
-
+from .exceptions import DimensionError
 
 def _check_fov(img, affine, shape):
     """ Return True if img's field of view correspond to given
@@ -37,7 +39,7 @@ def _check_same_fov(*args, **kwargs):
 
     args: images
         Images to be checked. Images passed without keywords will be labelled
-        as img_#1 in the error message (replace 1 with the appropriate index)
+        as img_#1 in the error message (replace 1 with the appropriate index).
 
     kwargs: images
         Images to be checked. In case of error, images will be reference by
@@ -153,7 +155,8 @@ def _iter_check_niimg(niimgs, ensure_ndim=None, atleast_4d=False,
 
 
 def check_niimg(niimg, ensure_ndim=None, atleast_4d=False, dtype=None,
-                return_iterator=False):
+                return_iterator=False,
+                wildcards=True):
     """Check that niimg is a proper 3D/4D niimg. Turn filenames into objects.
 
     Parameters
@@ -176,6 +179,17 @@ def check_niimg(niimg, ensure_ndim=None, atleast_4d=False, dtype=None,
         data will be converted to int32 if dtype is discrete and float32 if it
         is continuous.
 
+    return_iterator: boolean, optional
+        Returns an iterator on the content of the niimg file input
+
+    wildcards: boolean, optional
+        Use niimg as a regular expression to get a list of matching input
+        filenames.
+        If multiple files match, the returned list is sorted using an ascending
+        order.
+        If no file matches the regular expression, a ValueError exception is
+        raised.
+
     Returns
     -------
     result: 3D/4D Niimg-like object
@@ -193,6 +207,30 @@ def check_niimg(niimg, ensure_ndim=None, atleast_4d=False, dtype=None,
     """
     from ..image import new_img_like  # avoid circular imports
 
+    if isinstance(niimg, _basestring):
+        if wildcards and ni.EXPAND_PATH_WILDCARDS:
+            filenames = sorted(glob.glob(niimg))  # Ascending sorting
+
+            # processing filenames matching globbing expression
+            if len(filenames) >= 1 and glob.has_magic(niimg):
+                niimg = filenames  # iterable case
+            # niimg is an existing filename
+            elif [niimg] == filenames:
+                niimg = filenames[0]
+            # No files found by glob
+            elif glob.has_magic(niimg):
+                # No files matching the glob expression, warn the user
+                message = ("No files matching the entered niimg expression: "
+                            "'%s'.\n You may have left wildcards usage "
+                           "activated: please set the global constant "
+                           "'nilearn.EXPAND_PATH_WILDCARDS' to False to "
+                           "deactivate this behavior.") % niimg
+                raise ValueError(message)
+            else:
+                raise ValueError("File not found: '%s'" % niimg)
+        elif not os.path.exists(niimg):
+            raise ValueError("File not found: '%s'" % niimg)
+
     # in case of an iterable
     if hasattr(niimg, "__iter__") and not isinstance(niimg, _basestring):
         if return_iterator:
@@ -231,7 +269,7 @@ def check_niimg_3d(niimg, dtype=None):
         If niimg is a string, consider it as a path to Nifti image and
         call nibabel.load on it. If it is an object, check if get_data()
         and get_affine() methods are present, raise TypeError otherwise.
-    
+
     dtype: {dtype, "auto"}
         Data type toward which the data should be converted. If "auto", the
         data will be converted to int32 if dtype is discrete and float32 if it
@@ -272,7 +310,7 @@ def check_niimg_4d(niimg, return_iterator=False, dtype=None):
         Data type toward which the data should be converted. If "auto", the
         data will be converted to int32 if dtype is discrete and float32 if it
         is continuous.
-    
+
     return_iterator: boolean
         If True, an iterator of 3D images is returned. This reduces the memory
         usage when `niimgs` contains 3D images.


Test patch:
diff --git a/nilearn/_utils/testing.py b/nilearn/_utils/testing.py
--- a/nilearn/_utils/testing.py
+++ b/nilearn/_utils/testing.py
@@ -95,6 +95,10 @@ def write_tmp_imgs(*imgs, **kwargs):
         useful to test the two cases (filename / Nifti1Image) in the same
         loop.
 
+    use_wildcards: bool
+        if True, and create_files is True, imgs are written on disk and a
+        matching regexp is returned.
+
     Returns
     =======
     filenames: string or list of
@@ -102,7 +106,7 @@ def write_tmp_imgs(*imgs, **kwargs):
         has been given as input, a single string is returned. Otherwise, a
         list of string is returned.
     """
-    valid_keys = set(("create_files",))
+    valid_keys = set(("create_files", "use_wildcards"))
     input_keys = set(kwargs.keys())
     invalid_keys = input_keys - valid_keys
     if len(invalid_keys) > 0:
@@ -110,25 +114,34 @@ def write_tmp_imgs(*imgs, **kwargs):
                         (sys._getframe().f_code.co_name,
                          " ".join(invalid_keys)))
     create_files = kwargs.get("create_files", True)
+    use_wildcards = kwargs.get("use_wildcards", False)
+
+    prefix = "nilearn_"
+    suffix = ".nii"
 
     if create_files:
         filenames = []
-        with warnings.catch_warnings():
-            warnings.simplefilter("ignore", RuntimeWarning)
-            for img in imgs:
-                _, filename = tempfile.mkstemp(prefix="nilearn_",
-                                               suffix=".nii",
-                                               dir=None)
-                filenames.append(filename)
-                img.to_filename(filename)
-
-        if len(imgs) == 1:
-            yield filenames[0]
-        else:
-            yield filenames
-
-        for filename in filenames:
-            os.remove(filename)
+        try:
+            with warnings.catch_warnings():
+                warnings.simplefilter("ignore", RuntimeWarning)
+                for img in imgs:
+                    _, filename = tempfile.mkstemp(prefix=prefix,
+                                                   suffix=suffix,
+                                                   dir=None)
+                    filenames.append(filename)
+                    img.to_filename(filename)
+
+                if use_wildcards:
+                    yield prefix + "*" + suffix
+                else:
+                    if len(imgs) == 1:
+                        yield filenames[0]
+                    else:
+                        yield filenames
+        finally:
+            # Ensure all created files are removed
+            for filename in filenames:
+                os.remove(filename)
     else:  # No-op
         if len(imgs) == 1:
             yield imgs[0]
diff --git a/nilearn/tests/test_niimg_conversions.py b/nilearn/tests/test_niimg_conversions.py
--- a/nilearn/tests/test_niimg_conversions.py
+++ b/nilearn/tests/test_niimg_conversions.py
@@ -8,6 +8,7 @@
 # License: simplified BSD
 
 import os
+import re
 import tempfile
 
 from nose.tools import assert_equal, assert_true
@@ -18,6 +19,7 @@
 import nibabel
 from nibabel import Nifti1Image
 
+import nilearn as ni
 from nilearn import _utils, image
 from nilearn._utils.exceptions import DimensionError
 from nilearn._utils import testing, niimg_conversions
@@ -151,9 +153,9 @@ def test_check_niimg_4d():
         assert_array_equal(img_1.get_affine(), img_2.get_affine())
 
     # This should raise an error: a 3D img is given and we want a 4D
-    assert_raises_regex(DimensionError, 'Data must be a 4D Niimg-like object but '
-                        'you provided a 3D',
-                        _utils.check_niimg_4d, img_3d)
+    assert_raises_regex(DimensionError, 'Data must be a 4D Niimg-like object '
+                                        'but you provided a 3D',
+                                        _utils.check_niimg_4d, img_3d)
 
     # Test a Niimg-like object that does not hold a shape attribute
     phony_img = PhonyNiimage()
@@ -191,6 +193,119 @@ def test_check_niimg():
         'of 4D images.', _utils.check_niimg, img_2_4d, ensure_ndim=4)
 
 
+def test_check_niimg_wildcards():
+    tmp_dir = tempfile.tempdir + os.sep
+    nofile_path = "/tmp/nofile"
+    nofile_path_wildcards = "/tmp/no*file"
+    wildcards_msg = ("No files matching the entered niimg expression: "
+                     "'%s'.\n You may have left wildcards usage "
+                     "activated: please set the global constant "
+                     "'nilearn.EXPAND_PATH_WILDCARDS' to False to "
+                     "deactivate this behavior.")
+
+    file_not_found_msg = "File not found: '%s'"
+
+    assert_equal(ni.EXPAND_PATH_WILDCARDS, True)
+    # Check bad filename
+    # Non existing file (with no magic) raise a ValueError exception
+    assert_raises_regex(ValueError, file_not_found_msg % nofile_path,
+                        _utils.check_niimg, nofile_path)
+    # Non matching wildcard raises a ValueError exception
+    assert_raises_regex(ValueError,
+                        wildcards_msg % re.escape(nofile_path_wildcards),
+                        _utils.check_niimg, nofile_path_wildcards)
+
+    # First create some testing data
+    data_3d = np.zeros((40, 40, 40))
+    data_3d[20, 20, 20] = 1
+    data_3d_img = Nifti1Image(data_3d, np.eye(4))
+
+    data_4d = np.zeros((40, 40, 40, 3))
+    data_4d[20, 20, 20] = 1
+    data_4d_img = Nifti1Image(data_4d, np.eye(4))
+
+    #######
+    # Testing with an existing filename
+    with testing.write_tmp_imgs(data_3d_img, create_files=True) as filename:
+        assert_array_equal(_utils.check_niimg(filename).get_data(),
+                           data_3d_img.get_data())
+    # No globbing behavior
+    with testing.write_tmp_imgs(data_3d_img, create_files=True) as filename:
+        assert_array_equal(_utils.check_niimg(filename,
+                                              wildcards=False).get_data(),
+                           data_3d_img.get_data())
+
+    #######
+    # Testing with an existing filename
+    with testing.write_tmp_imgs(data_4d_img, create_files=True) as filename:
+        assert_array_equal(_utils.check_niimg(filename).get_data(),
+                           data_4d_img.get_data())
+    # No globbing behavior
+    with testing.write_tmp_imgs(data_4d_img, create_files=True) as filename:
+        assert_array_equal(_utils.check_niimg(filename,
+                                              wildcards=False).get_data(),
+                           data_4d_img.get_data())
+
+    #######
+    # Testing with a glob matching exactly one filename
+    # Using a glob matching one file containing a 3d image returns a 4d image
+    # with 1 as last dimension.
+    with testing.write_tmp_imgs(data_3d_img,
+                                create_files=True,
+                                use_wildcards=True) as globs:
+        glob_input = tmp_dir + globs
+        assert_array_equal(_utils.check_niimg(glob_input).get_data()[..., 0],
+                           data_3d_img.get_data())
+    # Disabled globbing behavior should raise an ValueError exception
+    with testing.write_tmp_imgs(data_3d_img,
+                                create_files=True,
+                                use_wildcards=True) as globs:
+        glob_input = tmp_dir + globs
+        assert_raises_regex(ValueError,
+                            file_not_found_msg % re.escape(glob_input),
+                            _utils.check_niimg,
+                            glob_input,
+                            wildcards=False)
+
+    #######
+    # Testing with a glob matching multiple filenames
+    img_4d = _utils.check_niimg_4d((data_3d_img, data_3d_img))
+    with testing.write_tmp_imgs(data_3d_img, data_3d_img,
+                                create_files=True,
+                                use_wildcards=True) as globs:
+        assert_array_equal(_utils.check_niimg(glob_input).get_data(),
+                           img_4d.get_data())
+
+    #######
+    # Test when global variable is set to False => no globbing allowed
+    ni.EXPAND_PATH_WILDCARDS = False
+
+    # Non existing filename (/tmp/nofile) could match an existing one through
+    # globbing but global wildcards variable overrides this feature => raises
+    # a ValueError
+    assert_raises_regex(ValueError,
+                        file_not_found_msg % nofile_path,
+                        _utils.check_niimg, nofile_path)
+
+    # Verify wildcards function parameter has no effect
+    assert_raises_regex(ValueError,
+                        file_not_found_msg % nofile_path,
+                        _utils.check_niimg, nofile_path, wildcards=False)
+
+    # Testing with an exact filename matching (3d case)
+    with testing.write_tmp_imgs(data_3d_img, create_files=True) as filename:
+        assert_array_equal(_utils.check_niimg(filename).get_data(),
+                           data_3d_img.get_data())
+
+    # Testing with an exact filename matching (4d case)
+    with testing.write_tmp_imgs(data_4d_img, create_files=True) as filename:
+        assert_array_equal(_utils.check_niimg(filename).get_data(),
+                           data_4d_img.get_data())
+
+    # Reverting to default behavior
+    ni.EXPAND_PATH_WILDCARDS = True
+
+
 def test_repr_niimgs():
     # Test with file path
     assert_equal(_utils._repr_niimgs("test"), "test")