Repository: mne-tools/mne-python

Issue:
apply_function argument error
The apply_function method of Raw objects currently accepts the following arguments:

```
self, fun, picks, dtype, n_jobs, verbose=None, *args, **kwargs
```

Unless I'm missing a trick on passing arguments to functions, this will throw an error unless you set verbosity level with a positional argument rather than a flag.

E.g., say you passed these arguments:

```
(my_fun, [0], None, 1, verbose=None, arg1, arg2=my_arg)
```

You'd raise the error that non-keyword arguments have been given after a keyword argument.

If you use this input:

```
(my_fun, [0], None, 1, arg1, arg2=my_arg)
```

Then it will assume that arg1 is the positional argument for "verbose" and will only pass arg2 to the function.

The only way I got this to work was to set verbose to a positional argument, e.g.:

```
(my_fun, [0], None, 1, None, arg1, arg2=my_arg)
```

If this is the only way of doing it, then verbose should either have to be a positional argument, or the way that arguments are handled should be a bit different. E.g., doesn't pandas have an "args" keyword where you pass a tuple with the positional arguments to pass to the function?



Discussion:
Maybe we should just do `(self, fun, picks. dtype, n_jobs, *args, **kwargs)`, keep `verbose` in the docstring, but then do `kwargs.pop('verbose', None)` in the code.

> Maybe we should just do (self, fun, picks. dtype, n_jobs, _args, *_kwargs),
> keep verbose in the docstring, but then do kwargs.pop('verbose', None) in
> the code.

if this works +1


Resolving patch:
diff --git a/mne/io/base.py b/mne/io/base.py
--- a/mne/io/base.py
+++ b/mne/io/base.py
@@ -199,6 +199,17 @@ def to_data_frame(self, picks=None, index=None, scale_time=1e3,
         return df
 
 
+def _check_fun(fun, d, *args, **kwargs):
+    want_shape = d.shape
+    d = fun(d, *args, **kwargs)
+    if not isinstance(d, np.ndarray):
+        raise TypeError('Return value must be an ndarray')
+    if d.shape != want_shape:
+        raise ValueError('Return data must have shape %s not %s'
+                         % (want_shape, d.shape))
+    return d
+
+
 class _BaseRaw(ProjMixin, ContainsMixin, PickDropChannelsMixin,
                SetChannelsMixin, InterpolationMixin, ToDataFrameMixin):
     """Base class for Raw data"""
@@ -307,8 +318,7 @@ def anonymize(self):
         self.info._anonymize()
 
     @verbose
-    def apply_function(self, fun, picks, dtype, n_jobs, verbose=None, *args,
-                       **kwargs):
+    def apply_function(self, fun, picks, dtype, n_jobs, *args, **kwargs):
         """ Apply a function to a subset of channels.
 
         The function "fun" is applied to the channels defined in "picks". The
@@ -332,25 +342,27 @@ def apply_function(self, fun, picks, dtype, n_jobs, verbose=None, *args,
             A function to be applied to the channels. The first argument of
             fun has to be a timeseries (numpy.ndarray). The function must
             return an numpy.ndarray with the same size as the input.
-        picks : array-like of int
-            Indices of channels to apply the function to.
+        picks : array-like of int | None
+            Indices of channels to apply the function to. If None, all
+            M-EEG channels are used.
         dtype : numpy.dtype
             Data type to use for raw data after applying the function. If None
             the data type is not modified.
         n_jobs: int
             Number of jobs to run in parallel.
-        verbose : bool, str, int, or None
-            If not None, override default verbose level (see mne.verbose).
-            Defaults to self.verbose.
         *args :
             Additional positional arguments to pass to fun (first pos. argument
             of fun is the timeseries of a channel).
         **kwargs :
-            Keyword arguments to pass to fun.
+            Keyword arguments to pass to fun. Note that if "verbose" is passed
+            as a member of ``kwargs``, it will be consumed and will override
+            the default mne-python verbose level (see mne.verbose).
         """
         if not self.preload:
             raise RuntimeError('Raw data needs to be preloaded. Use '
                                'preload=True (or string) in the constructor.')
+        if picks is None:
+            picks = pick_types(self.info, meg=True, eeg=True, exclude=[])
 
         if not callable(fun):
             raise ValueError('fun needs to be a function')
@@ -362,11 +374,12 @@ def apply_function(self, fun, picks, dtype, n_jobs, verbose=None, *args,
         if n_jobs == 1:
             # modify data inplace to save memory
             for idx in picks:
-                self._data[idx, :] = fun(data_in[idx, :], *args, **kwargs)
+                self._data[idx, :] = _check_fun(fun, data_in[idx, :],
+                                                *args, **kwargs)
         else:
             # use parallel function
-            parallel, p_fun, _ = parallel_func(fun, n_jobs)
-            data_picks_new = parallel(p_fun(data_in[p], *args, **kwargs)
+            parallel, p_fun, _ = parallel_func(_check_fun, n_jobs)
+            data_picks_new = parallel(p_fun(fun, data_in[p], *args, **kwargs)
                                       for p in picks)
             for pp, p in enumerate(picks):
                 self._data[p, :] = data_picks_new[pp]
diff --git a/mne/io/meas_info.py b/mne/io/meas_info.py
--- a/mne/io/meas_info.py
+++ b/mne/io/meas_info.py
@@ -1135,9 +1135,10 @@ def create_info(ch_names, sfreq, ch_types=None):
         Channel names.
     sfreq : float
         Sample rate of the data.
-    ch_types : list of str
+    ch_types : list of str | str
         Channel types. If None, data are assumed to be misc.
         Currently supported fields are "mag", "grad", "eeg", and "misc".
+        If str, then all channels are assumed to be of the same type.
 
     Notes
     -----
@@ -1154,6 +1155,8 @@ def create_info(ch_names, sfreq, ch_types=None):
     nchan = len(ch_names)
     if ch_types is None:
         ch_types = ['misc'] * nchan
+    if isinstance(ch_types, string_types):
+        ch_types = [ch_types] * nchan
     if len(ch_types) != nchan:
         raise ValueError('ch_types and ch_names must be the same length')
     info = _empty_info()
diff --git a/mne/utils.py b/mne/utils.py
--- a/mne/utils.py
+++ b/mne/utils.py
@@ -506,7 +506,7 @@ def _update_doc(self, olddoc):
 def verbose(function, *args, **kwargs):
     """Improved verbose decorator to allow functions to override log-level
 
-    Do not call this directly to set global verbosrity level, instead use
+    Do not call this directly to set global verbosity level, instead use
     set_log_level().
 
     Parameters
@@ -523,8 +523,10 @@ def verbose(function, *args, **kwargs):
     default_level = verbose_level = None
     if len(arg_names) > 0 and arg_names[0] == 'self':
         default_level = getattr(args[0], 'verbose', None)
-    if('verbose' in arg_names):
+    if 'verbose' in arg_names:
         verbose_level = args[arg_names.index('verbose')]
+    elif 'verbose' in kwargs:
+        verbose_level = kwargs.pop('verbose')
 
     # This ensures that object.method(verbose=None) will use object.verbose
     verbose_level = default_level if verbose_level is None else verbose_level


Test patch:
diff --git a/mne/io/tests/test_apply_function.py b/mne/io/tests/test_apply_function.py
new file mode 100644
--- /dev/null
+++ b/mne/io/tests/test_apply_function.py
@@ -0,0 +1,57 @@
+# Authors: Eric Larson <larson.eric.d@gmail.com>
+#
+# License: BSD (3-clause)
+
+import numpy as np
+import os.path as op
+from nose.tools import assert_equal, assert_raises
+
+from mne import create_info
+from mne.io import RawArray
+from mne.utils import logger, set_log_file, _TempDir
+
+
+def bad_1(x):
+    return  # bad return type
+
+
+def bad_2(x):
+    return x[:-1]  # bad shape
+
+
+def printer(x):
+    logger.info('exec')
+    return x
+
+
+def test_apply_function_verbose():
+    """Test apply function verbosity
+    """
+    n_chan = 2
+    n_times = 3
+    ch_names = [str(ii) for ii in range(n_chan)]
+    raw = RawArray(np.zeros((n_chan, n_times)),
+                   create_info(ch_names, 1., 'mag'))
+    # test return types in both code paths (parallel / 1 job)
+    assert_raises(TypeError, raw.apply_function, bad_1,
+                  None, None, 1)
+    assert_raises(ValueError, raw.apply_function, bad_2,
+                  None, None, 1)
+    assert_raises(TypeError, raw.apply_function, bad_1,
+                  None, None, 2)
+    assert_raises(ValueError, raw.apply_function, bad_2,
+                  None, None, 2)
+
+    # check our arguments
+    tempdir = _TempDir()
+    test_name = op.join(tempdir, 'test.log')
+    set_log_file(test_name)
+    try:
+        raw.apply_function(printer, None, None, 1, verbose=False)
+        with open(test_name) as fid:
+            assert_equal(len(fid.readlines()), 0)
+        raw.apply_function(printer, None, None, 1, verbose=True)
+        with open(test_name) as fid:
+            assert_equal(len(fid.readlines()), n_chan)
+    finally:
+        set_log_file(None)