Repository: microsoft/Qcodes

Issue:
Loop with simple function
In a simple measurement Loop I have used the `readnext()` function from the Keithley2700 driver. This gives are error 

```
TypeError: __call__() got an unexpected keyword argument 'first_delay'`
```

For parameters with custom `get_cmd` and `set_cmd` functions I have seem similar errors. It would be nice if we could have loops with simple get functions that take no arguments at all. Otherwise, we should document that all `get` and `set` functions must have `**kwargs`, but can safely discard keywords such as `first_delay`.



Discussion:
The situation is a bit more complicated. The following example shows the problem. The second `Loop` failes because in `qcodes/instrument/functions.py` the `Function.__call__` only accepts positional arguments (`args`), but not named arguments (`kwargs`).

Should we change `Function.__call__` (and possibly many similar functions) to accept named arguments? I cannot determine what the impact of such a change will be. On the other hand, I think that simple tasks (such as in the script below) should work without any trouble from the framework.

``` python
import numpy as np
import qcodes as qc
from qcodes import Instrument
from qcodes.utils.validators import Numbers as NumbersValidator

class KeithleyVirtual(Instrument):

    def __init__(self, name, address, reset=False, **kwargs):
        super().__init__(name, **kwargs)

        self.add_parameter('dummy',
                           get_cmd=self.get_dummy,
                           set_cmd=self.set_dummy,
                           vals=NumbersValidator())

        self.add_function('readnext', call_cmd=self.readnext_function,  units='arb.unit')

    def readnext_function(self, **kwargs):
        val=10 + np.random.rand() - .5
        return val

    def get_dummy(self, **kwargs):
        val=np.random.rand() - .5
        return val
    def set_dummy(self, value, **kwargs):
        pass


#%% Measure

k1 = KeithleyVirtual(name='keithley', address='', server_name=None)
sweepvalues=k1.dummy[-10:10:1]
station = qc.Station(k1)

station.set_measurement(k1.readnext_function)
data=qc.Loop(sweepvalues).run(background=False) # good


station.set_measurement(k1.readnext)
data=qc.Loop(sweepvalues).run(background=False) # fail
```

hmm, I can see why you'd want to do this, but I think it should be an error (not this error, of course, I should give a useful one!) When you want to measure something, it should match the `Parameter` API (so in general, it should actually be a `Parameter`) - that is, it's an object with a `get` method, but then it can also provide the `Loop` with other information like its name and label, whether or not it returns multiple values...

So I'd suggest for this use case, change `readnext` to a parameter like `measurement` or something:

```
self.add_parameter('measurement', get_cmd=readnext_function, units='arb. unit'...)
```

What's happening is, the `Loop` sees a callable that doesn't have a `get` method and thinks it's a `Task`, which does indeed ignore these kwargs, they're present to support nested loops. You'll notice that `data` didn't throw an error, but it didn't collect any data either (because a `Task` doesn't yield any data), it only gets the setpoints in `dummy`.

Ok. For me a parameter is something that is settable as well. For that reason I did not make the `readnext` a  `Parameter`. If we agree that this type of objects should be a `Parameter`, then I'll change my code as suggested. If we can make the system report a good error we can close this issue.


Resolving patch:
diff --git a/qcodes/loops.py b/qcodes/loops.py
--- a/qcodes/loops.py
+++ b/qcodes/loops.py
@@ -163,12 +163,33 @@ def each(self, *actions):
                 default = Station.default.default_measurement
                 actions[i] = action.each(*default)
 
+        self.validate_actions(*actions)
+
         if self.nested_loop:
             # recurse into the innermost loop and apply these actions there
             actions = [self.nested_loop.each(*actions)]
 
         return ActiveLoop(self.sweep_values, self.delay, *actions)
 
+    @staticmethod
+    def validate_actions(*actions):
+        """
+        Whitelist acceptable actions, so we can give nice error messages
+        if an action is not recognized
+        """
+        for action in actions:
+            if isinstance(action, (Task, Wait, ActiveLoop)):
+                continue
+            if hasattr(action, 'get') and (hasattr(action, 'name') or
+                                           hasattr(action, 'names')):
+                continue
+            raise TypeError('Unrecognized action:', action,
+                            'Allowed actions are: objects (parameters) with '
+                            'a `get` method and `name` or `names` attribute, '
+                            'and `Task`, `Wait`, and `ActiveLoop` objects. '
+                            '`Loop` objects are OK too, except in Station '
+                            'default measurements.')
+
     def run(self, *args, **kwargs):
         '''
         shortcut to run a loop with the default measurement set
@@ -516,10 +537,8 @@ def _compile_one(self, action, new_action_indices):
             return Task(self._wait, action.delay)
         elif isinstance(action, ActiveLoop):
             return _Nest(action, new_action_indices)
-        elif callable(action):
-            return action
         else:
-            raise TypeError('unrecognized action', action)
+            return action
 
     def _run_wrapper(self, *args, **kwargs):
         try:
diff --git a/qcodes/station.py b/qcodes/station.py
--- a/qcodes/station.py
+++ b/qcodes/station.py
@@ -57,6 +57,13 @@ def set_measurement(self, *actions):
         default Station, and any measurements among them can be done once
         by .measure
         '''
+        # Validate now so the user gets an error message ASAP
+        # and so we don't accept `Loop` as an action here, where
+        # it would cause infinite recursion.
+        # We need to import Loop inside here to avoid circular import
+        from .loops import Loop
+        Loop.validate_actions(*actions)
+
         self.default_measurement = actions
 
     def measure(self, *actions):


Test patch:
diff --git a/qcodes/tests/test_loop.py b/qcodes/tests/test_loop.py
--- a/qcodes/tests/test_loop.py
+++ b/qcodes/tests/test_loop.py
@@ -372,15 +372,38 @@ def test_composite_params(self):
         self.assertEqual(data.index1.tolist(), [[[0, 1]] * 2] * 2)
 
     def test_bad_actors(self):
-        # would be nice to find errors at .each, but for now we find them
-        # at .run
-        loop = Loop(self.p1[1:3:1], 0.001).each(self.p1, 42)
-        with self.assertRaises(TypeError):
-            loop.run_temp()
+        def f():
+            return 42
+
+        class NoName:
+            def get(self):
+                return 42
+
+        class HasName:
+            def get(self):
+                return 42
+
+            name = 'IHazName!'
+
+        class HasNames:
+            def get(self):
+                return 42
+
+            names = 'Namezz'
+
+        # first two minimal working gettables
+        Loop(self.p1[1:3:1]).each(HasName())
+        Loop(self.p1[1:3:1]).each(HasNames())
+
+        for bad_action in (f, 42, NoName()):
+            with self.assertRaises(TypeError):
+                # include a good action too, just to make sure we look
+                # at the whole list
+                Loop(self.p1[1:3:1]).each(self.p1, bad_action)
 
-        # at least invalid sweep values we find at .each
         with self.assertRaises(ValueError):
-            Loop(self.p1[-20:20:1], 0.001).each(self.p1)
+            # invalid sweep values
+            Loop(self.p1[-20:20:1]).each(self.p1)
 
     def test_very_short_delay(self):
         with LogCapture() as s: