Repository: nilearn/nilearn

Issue:
expandpath should be applied to the "memory" argument
The CacheMixin should be modified so that when the argument is a basestring, it is expanded, with the same logic to turn this off as the globbing that we already have.



Discussion:
@GaelVaroquaux, maybe I can work on that ?

Sounds great!  Thanks 

Sent from my phone. Please forgive brevity and mis spelling

On Mar 10, 2016, 09:37, at 09:37, Alexandre Abadie notifications@github.com wrote:

> @GaelVaroquaux, maybe I can work on that ?
> 
> ---
> 
> Reply to this email directly or view it on GitHub:
> https://github.com/nilearn/nilearn/issues/1051#issuecomment-194735399

To be complete, this should actually be applied to all paths in nilearn.

Just had a look at the code and I have a question: when the argument is a basestring, globbing may return a list a matching paths. Does the CacheMixin be modified to manage a list of Memories in this case ?

> basestring, globbing may return a list a matching paths.

We don't want globbing. Only exandpath.


Resolving patch:
diff --git a/nilearn/__init__.py b/nilearn/__init__.py
--- a/nilearn/__init__.py
+++ b/nilearn/__init__.py
@@ -43,8 +43,9 @@
     gzip.GzipFile.max_read_chunk = 100 * 1024 * 1024  # 100Mb
 
 # 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
+# and the os.path.expanduser usage in CacheMixin.
+# Default value it True, set it to False to completely deactivate this
+# behavior.
 EXPAND_PATH_WILDCARDS = True
 
 # Boolean controlling whether the joblib caches should be
diff --git a/nilearn/_utils/cache_mixin.py b/nilearn/_utils/cache_mixin.py
--- a/nilearn/_utils/cache_mixin.py
+++ b/nilearn/_utils/cache_mixin.py
@@ -208,17 +208,47 @@ def _cache(self, func, func_memory_level=1, **kwargs):
         if not hasattr(self, "memory"):
             self.memory = Memory(cachedir=None, verbose=verbose)
         if isinstance(self.memory, _basestring):
-            self.memory = Memory(cachedir=self.memory, verbose=verbose)
+            cache_dir = self.memory
+            if nilearn.EXPAND_PATH_WILDCARDS:
+                cache_dir = os.path.expanduser(cache_dir)
+
+            # Perform some verifications on given path.
+            split_cache_dir = os.path.split(cache_dir)
+            if (len(split_cache_dir) > 1 and
+                    (not os.path.exists(split_cache_dir[0]) and
+                     split_cache_dir[0] != '')):
+                if (not nilearn.EXPAND_PATH_WILDCARDS and
+                        cache_dir.startswith("~")):
+                    # Maybe the user want to enable expanded user path.
+                    error_msg = ("Given cache path parent directory doesn't "
+                                 "exists, you gave '{0}'. Enabling "
+                                 "nilearn.EXPAND_PATH_WILDCARDS could solve "
+                                 "this issue.".format(split_cache_dir[0]))
+                elif self.memory.startswith("~"):
+                    # Path built on top of expanded user path doesn't exist.
+                    error_msg = ("Given cache path parent directory doesn't "
+                                 "exists, you gave '{0}' which was expanded "
+                                 "as '{1}' but doesn't exist either. Use "
+                                 "nilearn.EXPAND_PATH_WILDCARDS to deactivate "
+                                 "auto expand user path (~) behavior."
+                                 .format(split_cache_dir[0],
+                                         os.path.dirname(self.memory)))
+                else:
+                    # The given cache base path doesn't exist.
+                    error_msg = ("Given cache path parent directory doesn't "
+                                 "exists, you gave '{0}'."
+                                 .format(split_cache_dir[0]))
+                raise ValueError(error_msg)
+
+            self.memory = Memory(cachedir=cache_dir, verbose=verbose)
 
         # If cache level is 0 but a memory object has been provided, set
         # memory_level to 1 with a warning.
-        if self.memory_level == 0:
-            if (isinstance(self.memory, _basestring)
-                    or self.memory.cachedir is not None):
-                warnings.warn("memory_level is currently set to 0 but "
-                              "a Memory object has been provided. "
-                              "Setting memory_level to 1.")
-                self.memory_level = 1
+        if self.memory_level == 0 and self.memory.cachedir is not None:
+            warnings.warn("memory_level is currently set to 0 but "
+                          "a Memory object has been provided. "
+                          "Setting memory_level to 1.")
+            self.memory_level = 1
 
         return cache(func, self.memory, func_memory_level=func_memory_level,
                      memory_level=self.memory_level, **kwargs)


Test patch:
diff --git a/nilearn/tests/test_cache_mixin.py b/nilearn/tests/test_cache_mixin.py
--- a/nilearn/tests/test_cache_mixin.py
+++ b/nilearn/tests/test_cache_mixin.py
@@ -12,7 +12,8 @@
 from sklearn.externals.joblib import Memory
 
 import nilearn
-from nilearn._utils import cache_mixin
+from nilearn._utils import cache_mixin, CacheMixin
+from nilearn._utils.testing import assert_raises_regex
 
 
 def f(x):
@@ -87,3 +88,67 @@ def test_cache_memory_level():
     assert_equal(len(glob.glob(job_glob)), 2)
     cache_mixin.cache(f, mem)(3)
     assert_equal(len(glob.glob(job_glob)), 3)
+
+
+class CacheMixinTest(CacheMixin):
+    """Dummy mock object that wraps a CacheMixin."""
+
+    def __init__(self, memory=None, memory_level=1):
+        self.memory = memory
+        self.memory_level = memory_level
+
+    def run(self):
+        self._cache(f)
+
+
+def test_cache_mixin_with_expand_user():
+    # Test the memory cache is correctly created when using ~.
+    cache_dir = "~/nilearn_data/test_cache"
+    expand_cache_dir = os.path.expanduser(cache_dir)
+    mixin_mock = CacheMixinTest(cache_dir)
+
+    try:
+        assert_false(os.path.exists(expand_cache_dir))
+        mixin_mock.run()
+        assert_true(os.path.exists(expand_cache_dir))
+    finally:
+        if os.path.exists(expand_cache_dir):
+            shutil.rmtree(expand_cache_dir)
+
+
+def test_cache_mixin_without_expand_user():
+    # Test the memory cache is correctly created when using ~.
+    cache_dir = "~/nilearn_data/test_cache"
+    expand_cache_dir = os.path.expanduser(cache_dir)
+    mixin_mock = CacheMixinTest(cache_dir)
+
+    try:
+        assert_false(os.path.exists(expand_cache_dir))
+        nilearn.EXPAND_PATH_WILDCARDS = False
+        assert_raises_regex(ValueError,
+                            "Given cache path parent directory doesn't",
+                            mixin_mock.run)
+        assert_false(os.path.exists(expand_cache_dir))
+        nilearn.EXPAND_PATH_WILDCARDS = True
+    finally:
+        if os.path.exists(expand_cache_dir):
+            shutil.rmtree(expand_cache_dir)
+
+
+def test_cache_mixin_wrong_dirs():
+    # Test the memory cache raises a ValueError when input base path doesn't
+    # exist.
+
+    for cache_dir in ("/bad_dir/cache",
+                      "~/nilearn_data/tmp/test_cache"):
+        expand_cache_dir = os.path.expanduser(cache_dir)
+        mixin_mock = CacheMixinTest(cache_dir)
+
+        try:
+            assert_raises_regex(ValueError,
+                                "Given cache path parent directory doesn't",
+                                mixin_mock.run)
+            assert_false(os.path.exists(expand_cache_dir))
+        finally:
+            if os.path.exists(expand_cache_dir):
+                shutil.rmtree(expand_cache_dir)