Repository: nilearn/nilearn

Issue:
`TypeError: expected string or bytes-like object` when using `nilearn.input_data.NiftiMasker` with `memory` parameter.
I get the following error from a downstream test function which used to work: https://ppb.chymera.eu/cc4f38.log

It gets fixed by changing:

```python
my_memory = Memory(path.abspath(path.expanduser('~/.nilearn_cache')))
masker = NiftiMasker(mask_img=roi, target_affine=ts_img.affine, memory=my_memory, memory_level=1)
```

to:

```python
masker = NiftiMasker(mask_img=roi, target_affine=ts_img.affine, memory_level=1)
```

Given that it seems to fail with a version comparison I suspect this is a fairly obvious issue with the installation? Any idea how to fix it, though?


Discussion:
It looks like this also came up on NeuroStars: https://neurostars.org/t/typeerror-for-fit-transform/22458. Based on that thread, I don't think anyone knows how to fix it yet.
A member in my lab reported similar error from a customised masker built from nilearn maskers. 
I am trying to replicate this error but failing so far. (python version 3.8)
Yep, we need to understand whether this comes from a change on the joblib side.
Thx for posting.
Looking at the logs, it seems to be a bug from `nilearn` side, specifically in the version check from the `_safe_cache`:

https://github.com/nilearn/nilearn/blob/4242c0d03e3fc790b17a1d88ce5a4140633c6b6f/nilearn/_utils/cache_mixin.py#L111-L117

The test fails because one of the module does not have a `str` version but a `list` one.

Thx @tomMoral !
Here we have some progress - a virtual environment created on our HPC leads to this error but not on a local client machine.
AFAIK, on our cluster, pip will prioritise the system version for some packages, instead of fetching the latest. 
I don't have time to look into this issue further this week... Can revisit this in June.
Hi @htwangtw I'm wondering if we can revive this ahead of the next release. Have you had the chance to look into it any further?
Sorry I haven't had time to dig further @ymzayek 
This will be moved to my priority list once I have a paper wrapped up.
Just for our organization: should we count it as part of the next release (0.10) ? 
from office hours discussion we may simply change the version check to make it handle the case where one version has incorrect type and say that they don't match in that case.

also that  function loops over a list of modules that only contains nibabel so it can be simplified.

we may want to investigate if we still need this check or if it was just a fix for some issue in old less stable versions of nibabel


Resolving patch:
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
@@ -4,13 +4,8 @@
 # Author: Gael Varoquaux, Alexandre Abraham, Philippe Gervais
 # License: simplified BSD
 
-import json
 import warnings
 import os
-import shutil
-from nilearn.version import _compare_version
-
-import nibabel
 
 from joblib import Memory
 
@@ -20,9 +15,6 @@
 from .helpers import stringify_path
 
 
-__CACHE_CHECKED = dict()
-
-
 def _check_memory(memory, verbose=0):
     """Function to ensure an instance of a joblib.Memory object.
 
@@ -80,82 +72,6 @@ def _check_memory(memory, verbose=0):
     return memory
 
 
-def _safe_cache(memory, func, **kwargs):
-    """A wrapper for mem.cache that flushes the cache if the version
-    number of nibabel has changed.
-
-    """
-    ''' Workaround for
-     https://github.com/scikit-learn-contrib/imbalanced-learn/issues/482
-    joblib throws a spurious warning with newer scikit-learn.
-    This code uses the recommended method first and the deprecated one
-    if that fails, ensuring th warning is not generated in any case.
-    '''
-    try:
-        location = os.path.join(memory.location, 'joblib')
-    except AttributeError:
-        location = memory.location
-    except TypeError:
-        location = None
-
-    if location is None or location in __CACHE_CHECKED:
-        return memory.cache(func, **kwargs)
-
-    version_file = os.path.join(location, 'module_versions.json')
-
-    versions = dict()
-    if os.path.exists(version_file):
-        with open(version_file, 'r') as _version_file:
-            versions = json.load(_version_file)
-
-    modules = (nibabel, )
-    # Keep only the major + minor version numbers
-    my_versions = dict((m.__name__, m.__version__) for m in modules)
-    commons = set(versions.keys()).intersection(set(my_versions.keys()))
-    collisions = [
-        m for m in commons if not _compare_version(
-            versions[m], '==', my_versions[m]
-        )
-    ]
-    # Flush cache if version collision
-    if len(collisions) > 0:
-        if nilearn.CHECK_CACHE_VERSION:
-            warnings.warn("Incompatible cache in %s: "
-                          "different version of nibabel. Deleting "
-                          "the cache. Put nilearn.CHECK_CACHE_VERSION "
-                          "to false to avoid this behavior."
-                          % location)
-            try:
-                tmp_dir = (os.path.split(location)[:-1]
-                           + ('old_%i' % os.getpid(), ))
-                tmp_dir = os.path.join(*tmp_dir)
-                # We use rename + unlink to be more robust to race
-                # conditions
-                os.rename(location, tmp_dir)
-                shutil.rmtree(tmp_dir)
-            except OSError:
-                # Another process could have removed this dir
-                pass
-
-            try:
-                os.makedirs(location)
-            except OSError:
-                # File exists?
-                pass
-        else:
-            warnings.warn("Incompatible cache in %s: "
-                          "old version of nibabel." % location)
-
-    # Write json files if configuration is different
-    if versions != my_versions:
-        with open(version_file, 'w') as _version_file:
-            json.dump(my_versions, _version_file)
-
-    __CACHE_CHECKED[location] = True
-
-    return memory.cache(func, **kwargs)
-
-
 class _ShelvedFunc:
     """Work around for Python 2, for which pickle fails on instance method"""
     def __init__(self, func):
@@ -240,7 +156,7 @@ def cache(func, memory, func_memory_level=None, memory_level=None,
                           stacklevel=2)
     else:
         memory = Memory(location=None, verbose=verbose)
-    cached_func = _safe_cache(memory, func, **kwargs)
+    cached_func = memory.cache(func, **kwargs)
     if shelve:
         cached_func = _ShelvedFunc(cached_func)
     return cached_func


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
@@ -45,51 +45,6 @@ def test_check_memory():
             assert memory, Memory
 
 
-def test__safe_cache_dir_creation():
-    # Test the _safe_cache function that is supposed to flush the
-    # cache if the nibabel version changes
-    with tempfile.TemporaryDirectory() as temp_dir:
-        mem = Memory(location=temp_dir)
-        version_file = os.path.join(temp_dir, 'joblib', 'module_versions.json')
-        assert not os.path.exists(version_file)
-        # First test that a version file get created
-        cache_mixin._safe_cache(mem, f)
-        assert os.path.exists(version_file)
-        # Test that it does not get recreated during the same session
-        os.unlink(version_file)
-        cache_mixin._safe_cache(mem, f)
-        assert not os.path.exists(version_file)
-
-
-def test__safe_cache_flush():
-    # Test the _safe_cache function that is supposed to flush the
-    # cache if the nibabel version changes
-    with tempfile.TemporaryDirectory() as temp_dir:
-        mem = Memory(location=temp_dir)
-        version_file = os.path.join(temp_dir, 'joblib', 'module_versions.json')
-        # Create an mock version_file with old module versions
-        with open(version_file, 'w') as f:
-            json.dump({"nibabel": "0.0"}, f)
-        # Create some store structure
-        nibabel_dir = os.path.join(temp_dir, 'joblib', 'nibabel_')
-        os.makedirs(nibabel_dir)
-
-        # First turn off version checking
-        nilearn.CHECK_CACHE_VERSION = False
-        cache_mixin._safe_cache(mem, f)
-        assert os.path.exists(nibabel_dir)
-
-        # Second turn on version checking
-        nilearn.CHECK_CACHE_VERSION = True
-        # Make sure that the check will run again
-        cache_mixin.__CACHE_CHECKED = {}
-        with open(version_file, 'w') as f:
-            json.dump({"nibabel": "0.0"}, f)
-        cache_mixin._safe_cache(mem, f)
-        assert os.path.exists(version_file)
-        assert not os.path.exists(nibabel_dir)
-
-
 def test_cache_memory_level():
     with tempfile.TemporaryDirectory() as temp_dir:
         joblib_dir = Path(