Repository: mne-tools/mne-python

Issue:
Cannot pickle mne.preprocessing.ica.ICA anymore
With the current master (installed via `pip install git+https://github.com/mne-tools/mne-python.git#egg=mne`), I get an error when pickling an `mne.preprocessing.ica.ICA` object:

```
import pickle
import mne


print(mne.__version__)
ica = mne.preprocessing.ICA()
raw = mne.io.read_raw_edf("/Users/clemens/Downloads/testfiles/S001R04.edf", preload=True)
ica.fit(raw)
pickle.loads(pickle.dumps(ica))
# Traceback (most recent call last):
#   File "/Users/clemens/Repositories/mnelab/.venv/lib/python3.7/site-packages/IPython/core/interactiveshell.py", line 3296, in run_code
#     exec(code_obj, self.user_global_ns, self.user_ns)
#   File "<ipython-input-8-a056623ee708>", line 1, in <module>
#     pickle.loads(pickle.dumps(ica))
# TypeError: __new__() missing 1 required positional argument: 'val'
```

The same code works with the latest released version 0.17.2. I didn't see any change to the `ICA` object that might be causing this (but maybe I'm missing something). If indeed nothing changed, could this behavior be due to how pip installs stuff from GitHub (vs. "proper" packages from PyPI)? If so, how can I fix this (I need to pickle because I want to use the `multiprocessing` module)?


Discussion:
that's weird...

Is this 100% reproducible on a diff, fresh computer?

Uh ... why can't you write the ICA objects to disk the normal way?
I would blame the utils/_bunch.py..
here is a full snippet to replicate the pb:

import mne
import pickle
ica = mne.preprocessing.ICA()
raw = mne.io.read_raw_edf(mne.get_config('MNE_DATA') +
"/MNE-eegbci-data/physiobank/database/eegmmidb/S001/S001R04.edf",
preload=True)
ica.fit(raw)
pickle.loads(pickle.dumps(ica))

any help welcome.

This is expected behavior when defining custom \_\_new\_\_-methods.

You could add something like this to utils/_bunch.py 's NamedInt:
` 
    def __getnewargs__(obj):
        return obj._name, int(obj)
`



Resolving patch:
diff --git a/mne/utils/_bunch.py b/mne/utils/_bunch.py
--- a/mne/utils/_bunch.py
+++ b/mne/utils/_bunch.py
@@ -9,10 +9,10 @@
 
 
 ###############################################################################
-# Create a Bunch class that acts like a struct (mybunch.key = val )
+# Create a Bunch class that acts like a struct (mybunch.key = val)
 
 class Bunch(dict):
-    """Dictionnary-like object thatexposes its keys as attributes."""
+    """Dictionary-like object that exposes its keys as attributes."""
 
     def __init__(self, **kwargs):  # noqa: D102
         dict.__init__(self, kwargs)
@@ -51,9 +51,14 @@ def __setattr__(self, attr, val):  # noqa: D105
         super().__setattr__(attr, val)
 
 
-class Named(object):
+class _Named(object):
     """Provide shared methods for giving named-representation subclasses."""
 
+    def __new__(cls, name, val):  # noqa: D102,D105
+        out = _named_subclass(cls).__new__(cls, val)
+        out._name = name
+        return out
+
     def __str__(self):  # noqa: D105
         return '%s (%s)' % (super().__str__(), self._name)
 
@@ -74,20 +79,25 @@ def __deepcopy__(self, memo):  # noqa: D105
             setattr(result, k, deepcopy(v, memo))
         return result
 
+    def __getnewargs__(self):  # noqa: D105
+        return self._name, _named_subclass(self)(self)
+
+
+def _named_subclass(klass):
+    if not isinstance(klass, type):
+        klass = klass.__class__
+    subklass = klass.mro()[-2]
+    assert subklass in (int, float)
+    return subklass
+
 
-class NamedInt(Named, int):
+class NamedInt(_Named, int):
     """Int with a name in __repr__."""
 
-    def __new__(cls, name, val):  # noqa: D102,D105
-        out = int.__new__(cls, val)
-        out._name = name
-        return out
+    pass
 
 
-class NamedFloat(Named, float):
+class NamedFloat(_Named, float):
     """Float with a name in __repr__."""
 
-    def __new__(cls, name, val):  # noqa: D102,D105
-        out = float.__new__(cls, val)
-        out._name = name
-        return out
+    pass


Test patch:
diff --git a/mne/utils/tests/test_bunch.py b/mne/utils/tests/test_bunch.py
new file mode 100644
--- /dev/null
+++ b/mne/utils/tests/test_bunch.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Authors: Clemens Brunner <clemens.brunner@gmail.com>
+#          Eric Larson <larson.eric.d@gmail.com>
+#
+# License: BSD (3-clause)
+
+import pickle
+from mne.utils import BunchConstNamed
+from mne.utils._bunch import NamedInt, NamedFloat
+
+
+def test_pickle():
+    """Test if BunchConstNamed object can be pickled."""
+    b1 = BunchConstNamed()
+    b1.x = 1
+    b1.y = 2.12
+    assert isinstance(b1.x, int)
+    assert isinstance(b1.x, NamedInt)
+    assert repr(b1.x) == '1 (x)'
+    assert isinstance(b1.y, float)
+    assert isinstance(b1.y, NamedFloat)
+    assert repr(b1.y) == '2.12 (y)'
+
+    b2 = pickle.loads(pickle.dumps(b1))
+    assert b1 == b2