Repository: nipy/nipype

Issue:
[BUG] Problems hashing inputs derived from encoding in corner cases
The new feature of reading settings from a json file (#1591) has introduced/uncovered the following issue derived from encoding when hashing inputs:

```
from nipype.interfaces.ants import Registration

tsthash = Registration(from_file='examples/smri_ants_registration_settings.json')  
tsthash.inputs.get_hashval(hash_method='timestamp')
```

This leads (py27, at the very least) to:

```
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-12-ed85d387f813> in <module>()
----> 1 tsthash.inputs.get_hashval(hash_method='timestamp')

/home/oesteban/workspace/nipype/nipype/interfaces/base.pyc in get_hashval(self, hash_method)
    574                                       self._get_sorteddict(val, True, hash_method=hash_method,
    575                                                            hash_files=hash_files)))
--> 576         return dict_withhash, md5(encode_dict(dict_nofilename).encode()).hexdigest()
    577 
    578     def _get_sorteddict(self, object, dictwithhash=False, hash_method=None,

/home/oesteban/workspace/nipype/nipype/utils/filemanip.pyc in encode_dict(value)
    105             if i > 0:
    106                 retval += ', '
--> 107             retval += encode_dict(v)
    108         return retval + ']'
    109     return repr(value)

/home/oesteban/workspace/nipype/nipype/utils/filemanip.pyc in encode_dict(value)
     97     if isinstance(value, tuple):
     98         val0 = encode_dict(value[0])
---> 99         val1 = encode_dict(value[1])
    100         return '(' + val0 + ', ' + val1 + ')'
    101 

/home/oesteban/workspace/nipype/nipype/utils/filemanip.pyc in encode_dict(value)
    105             if i > 0:
    106                 retval += ', '
--> 107             retval += encode_dict(v)
    108         return retval + ']'
    109     return repr(value)

/home/oesteban/workspace/nipype/nipype/utils/filemanip.pyc in encode_dict(value)
     97     if isinstance(value, tuple):
     98         val0 = encode_dict(value[0])
---> 99         val1 = encode_dict(value[1])
    100         return '(' + val0 + ', ' + val1 + ')'
    101 

IndexError: tuple index out of range
```

I'm already working on the patch.

~~If the `from_file` feature is not used everything will work fine.~~

[BUG] New initialization of interfaces with from_file fails with lists of lists of ints



Discussion:

Resolving patch:
diff --git a/examples/smri_ants_registration.py b/examples/smri_ants_registration.py
--- a/examples/smri_ants_registration.py
+++ b/examples/smri_ants_registration.py
@@ -24,6 +24,7 @@
 import urllib.error
 import urllib.parse
 from nipype.interfaces.ants import Registration
+from nipype.testing import example_data
 
 """
 2. Download T1 volumes into home directory
@@ -61,11 +62,12 @@
 
 """
 3. Define the parameters of the registration. Settings are
-saved in ``smri_ants_registration_settings.json``.
+found in the file ``smri_ants_registration_settings.json``
+distributed with the ``example_data`` of `nipype`.
 
 """
 
-reg = Registration(from_file='./smri_ants_registration_settings.json')
+reg = Registration(from_file=example_data('smri_ants_registration_settings.json'))
 reg.inputs.fixed_image = input_images[0]
 reg.inputs.moving_image = input_images[1]
 
diff --git a/nipype/interfaces/base.py b/nipype/interfaces/base.py
--- a/nipype/interfaces/base.py
+++ b/nipype/interfaces/base.py
@@ -47,6 +47,7 @@
 nipype_version = LooseVersion(__version__)
 iflogger = logging.getLogger('interface')
 
+FLOAT_FORMAT = '{:.10f}'.format
 PY35 = sys.version_info >= (3, 5)
 PY3 = sys.version_info[0] > 2
 
@@ -536,6 +537,14 @@ def _clean_container(self, object, undefinedval=None, skipundefined=False):
                     out = undefinedval
         return out
 
+    def has_metadata(self, name, metadata, value=None, recursive=True):
+        """
+        Return has_metadata for the requested trait name in this
+        interface
+        """
+        return has_metadata(self.trait(name).trait_type, metadata, value,
+                            recursive)
+
     def get_hashval(self, hash_method=None):
         """Return a dictionary of our items with hashes for each file.
 
@@ -560,61 +569,61 @@ def get_hashval(self, hash_method=None):
         dict_withhash = []
         dict_nofilename = []
         for name, val in sorted(self.get().items()):
-            if isdefined(val):
-                trait = self.trait(name)
-                if has_metadata(trait.trait_type, "nohash", True):
-                    continue
-                hash_files = (not has_metadata(trait.trait_type, "hash_files",
-                                               False) and not
-                              has_metadata(trait.trait_type, "name_source"))
-                dict_nofilename.append((name,
-                                        self._get_sorteddict(val, hash_method=hash_method,
-                                                             hash_files=hash_files)))
-                dict_withhash.append((name,
-                                      self._get_sorteddict(val, True, hash_method=hash_method,
-                                                           hash_files=hash_files)))
+            if not isdefined(val) or self.has_metadata(name, "nohash", True):
+                # skip undefined traits and traits with nohash=True
+                continue
+
+            hash_files = (not self.has_metadata(name, "hash_files", False) and not
+                          self.has_metadata(name, "name_source"))
+            dict_nofilename.append((name,
+                                    self._get_sorteddict(val, hash_method=hash_method,
+                                                         hash_files=hash_files)))
+            dict_withhash.append((name,
+                                  self._get_sorteddict(val, True, hash_method=hash_method,
+                                                       hash_files=hash_files)))
         return dict_withhash, md5(encode_dict(dict_nofilename).encode()).hexdigest()
 
-    def _get_sorteddict(self, object, dictwithhash=False, hash_method=None,
+
+    def _get_sorteddict(self, objekt, dictwithhash=False, hash_method=None,
                         hash_files=True):
-        if isinstance(object, dict):
+        if isinstance(objekt, dict):
             out = []
-            for key, val in sorted(object.items()):
+            for key, val in sorted(objekt.items()):
                 if isdefined(val):
                     out.append((key,
                                 self._get_sorteddict(val, dictwithhash,
                                                      hash_method=hash_method,
                                                      hash_files=hash_files)))
-        elif isinstance(object, (list, tuple)):
+        elif isinstance(objekt, (list, tuple)):
             out = []
-            for val in object:
+            for val in objekt:
                 if isdefined(val):
                     out.append(self._get_sorteddict(val, dictwithhash,
                                                     hash_method=hash_method,
                                                     hash_files=hash_files))
-            if isinstance(object, tuple):
+            if isinstance(objekt, tuple):
                 out = tuple(out)
         else:
-            if isdefined(object):
-                if (hash_files and isinstance(object, (str, bytes)) and
-                        os.path.isfile(object)):
+            if isdefined(objekt):
+                if (hash_files and isinstance(objekt, (str, bytes)) and
+                        os.path.isfile(objekt)):
                     if hash_method is None:
                         hash_method = config.get('execution', 'hash_method')
 
                     if hash_method.lower() == 'timestamp':
-                        hash = hash_timestamp(object)
+                        hash = hash_timestamp(objekt)
                     elif hash_method.lower() == 'content':
-                        hash = hash_infile(object)
+                        hash = hash_infile(objekt)
                     else:
                         raise Exception("Unknown hash method: %s" % hash_method)
                     if dictwithhash:
-                        out = (object, hash)
+                        out = (objekt, hash)
                     else:
                         out = hash
-                elif isinstance(object, float):
-                    out = '%.10f' % object
+                elif isinstance(objekt, float):
+                    out = FLOAT_FORMAT(objekt)
                 else:
-                    out = object
+                    out = objekt
         return out
 
 
@@ -769,7 +778,10 @@ def __init__(self, from_file=None, **inputs):
         self.num_threads = 1
 
         if from_file is not None:
-            self.load_inputs_from_json(from_file, overwrite=False)
+            self.load_inputs_from_json(from_file, overwrite=True)
+
+            for name, value in list(inputs.items()):
+                setattr(self.inputs, name, value)
 
 
     @classmethod
diff --git a/nipype/utils/filemanip.py b/nipype/utils/filemanip.py
--- a/nipype/utils/filemanip.py
+++ b/nipype/utils/filemanip.py
@@ -89,24 +89,38 @@ def encode_dict(value):
 
     """
     if sys.version_info[0] > 2:
-        return str(value)
-
-    if isinstance(value, str):
-        value = value.encode()
+        retval = str(value)
+    else:
+        retval = encode_dict_py27(value)
+    return retval
 
-    if isinstance(value, tuple):
-        val0 = encode_dict(value[0])
-        val1 = encode_dict(value[1])
-        return '(' + val0 + ', ' + val1 + ')'
+def encode_dict_py27(value):
+    """
+    Encode dictionary for python 2
+    """
 
-    if isinstance(value, list):
-        retval = '['
+    istuple = isinstance(value, tuple)
+    if isinstance(value, (tuple, list)):
+        retval = '(' if istuple else '['
+        nels = len(value)
         for i, v in enumerate(value):
-            if i > 0:
+            venc = encode_dict_py27(v)
+            if venc.startswith("u'") or venc.startswith('u"'):
+                venc = venc[1:]
+            retval += venc
+
+            if i < nels - 1:
                 retval += ', '
-            retval += encode_dict(v)
-        return retval + ']'
-    return repr(value)
+
+        if istuple and nels == 1:
+            retval += ','
+        retval += ')' if istuple else ']'
+        return retval
+
+    retval = repr(value).decode()
+    if retval.startswith("u'") or retval.startswith('u"'):
+        retval = retval[1:]
+    return retval
 
 def fname_presuffix(fname, prefix='', suffix='', newpath=None, use_ext=True):
     """Manipulates path and name of input filename


Test patch:
diff --git a/nipype/interfaces/tests/test_base.py b/nipype/interfaces/tests/test_base.py
--- a/nipype/interfaces/tests/test_base.py
+++ b/nipype/interfaces/tests/test_base.py
@@ -10,10 +10,11 @@
 import tempfile
 import shutil
 import warnings
+import simplejson as json
 
 from nipype.testing import (assert_equal, assert_not_equal, assert_raises,
                             assert_true, assert_false, with_setup, package_check,
-                            skipif)
+                            skipif, example_data)
 import nipype.interfaces.base as nib
 from nipype.utils.filemanip import split_filename
 from nipype.interfaces.base import Undefined, config
@@ -497,10 +498,21 @@ def __init__(self, **inputs):
     bif6.load_inputs_from_json(tmp_json)
     yield assert_equal, bif6.inputs.get_traitsfree(), inputs_dict
 
-def assert_not_raises(fn, *args, **kwargs):
-    fn(*args, **kwargs)
-    return True
+    # test get hashval in a complex interface
+    from nipype.interfaces.ants import Registration
+    settings = example_data(example_data('smri_ants_registration_settings.json'))
+    with open(settings) as setf:
+        data_dict = json.load(setf)
+
+    tsthash = Registration()
+    tsthash.load_inputs_from_json(settings)
+    yield assert_equal, {}, check_dict(data_dict, tsthash.inputs.get_traitsfree())
 
+    tsthash2 = Registration(from_file=settings)
+    yield assert_equal, {}, check_dict(data_dict, tsthash2.inputs.get_traitsfree())
+
+    _, hashvalue = tsthash.inputs.get_hashval(hash_method='timestamp')
+    yield assert_equal, 'ec5755e07287e04a4b409e03b77a517c', hashvalue
 
 def test_input_version():
     class InputSpec(nib.TraitedSpec):
@@ -738,3 +750,27 @@ def test_global_CommandLine_output():
     yield assert_equal, res.runtime.stdout, ''
     os.chdir(pwd)
     teardown_file(tmpd)
+
+def assert_not_raises(fn, *args, **kwargs):
+    fn(*args, **kwargs)
+    return True
+
+def check_dict(ref_dict, tst_dict):
+    """Compare dictionaries of inputs and and those loaded from json files"""
+    def to_list(x):
+        if isinstance(x, tuple):
+            x = list(x)
+
+        if isinstance(x, list):
+            for i, xel in enumerate(x):
+                x[i] = to_list(xel)
+
+        return x
+
+    failed_dict = {}
+    for key, value in list(ref_dict.items()):
+        newval = to_list(tst_dict[key])
+        if newval != value:
+            failed_dict[key] = (value, newval)
+    return failed_dict
+
diff --git a/examples/smri_ants_registration_settings.json b/nipype/testing/data/smri_ants_registration_settings.json
similarity index 100%
rename from examples/smri_ants_registration_settings.json
rename to nipype/testing/data/smri_ants_registration_settings.json
diff --git a/nipype/utils/tests/test_filemanip.py b/nipype/utils/tests/test_filemanip.py
--- a/nipype/utils/tests/test_filemanip.py
+++ b/nipype/utils/tests/test_filemanip.py
@@ -10,11 +10,11 @@
 
 from ...testing import assert_equal, assert_true, assert_false, TempFATFS
 from ...utils.filemanip import (save_json, load_json,
-                                    fname_presuffix, fnames_presuffix,
-                                    hash_rename, check_forhash,
-                                    copyfile, copyfiles,
-                                    filename_to_list, list_to_filename,
-                                    split_filename, get_related_files)
+                                fname_presuffix, fnames_presuffix,
+                                hash_rename, check_forhash,
+                                copyfile, copyfiles,
+                                filename_to_list, list_to_filename,
+                                split_filename, get_related_files)
 
 import numpy as np