Repository: microsoft/Qcodes

Issue:
Using `Lists` validator in `MultiType` Validator throws exception

### Steps to reproduce
run this
```
python -c "from qcodes.utils.validators import Ints, Lists, MultiType; MultiType(Lists(Ints()), Ints())"
```

### Expected behaviour
`MultiType(Lists(Ints()), Ints())` should return a MultiType validator that accepts Lists of Ints or Ints.

### Actual behaviour
The above snippet throws a TypeError due to `class list` being not hashable.
```
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/Users/stefan/PycharmProjects/Qcodes/qcodes/utils/validators.py", line 467, in __init__
    self._valid_values = list(set(self._valid_values))
TypeError: unhashable type: 'list'
```

### System
OSX 10.14
Python 3.7
Qcodes git (master)


### My thoughts:
Further looking at the instruction where the error is thrown shows that the error occurs when the `self._valid_values` is converted to a set (in order to only keep unique values). Since the type of `Lists._valid_values` is a "list of lists" the implicit unpacking (by `+=`) of the list of lists that happens in Line 466  results in a list being appended to `MultiType._valid_values`

https://github.com/QCoDeS/Qcodes/blob/94f271b6c14599ed2bd7e303e77c993bb46c44fc/qcodes/utils/validators.py#L463-L467

From my point of view the best way to fix this is that `_valid_values` should always be a tuple instead of a list. However I have not looked at the details of the logic of what _valid_values is used for, so maybe it makes sense to have a list somewhere?


Discussion:
@StefanD986, thanks for reporting this. And **congratulations** for opening issue number `1337`! 😄 

I like the idea of using tuples instead of lists for holding the `valid_values`, but I don't think there is any way around the `valid_values` for `Lists` being anything but a list. I'll post a PR shortly with an attempted solution.

Resolving patch:
diff --git a/qcodes/utils/validators.py b/qcodes/utils/validators.py
--- a/qcodes/utils/validators.py
+++ b/qcodes/utils/validators.py
@@ -56,7 +56,7 @@ class Validator:
     each validator should implement:
 
     __init__: here a private attribute, _valid_values, should be set.
-        _valid_values must be a list of at least one valid value.
+        _valid_values must be a tuple of at least one valid value.
         If possible, it should include all valid values. The purpose of
         this attribute is to make it possible to find a valid value for
         a Parameter, given its validator.
@@ -71,23 +71,23 @@ class Validator:
 
     The base class implements:
 
-    valid_values: a property exposing _valid_values, which is a list
+    valid_values: a property exposing _valid_values, which is a tuple
         of examples of valid values. For very simple validators, like
-        Bool or Enum, the list contains all valid values, but in general
+        Bool or Enum, the tuple contains all valid values, but in general
         it just holds SOME valid values. These example values are intended
         to be useful when simulating instruments.
 
     Alternatively you may override valid_values and provide your own
     implementation of getting valid values.
     """
-    _valid_values: Any = None
+    _valid_values: Tuple = ()
     is_numeric = False  # is this a numeric type (so it can be swept)?
 
     def validate(self, value, context: str=''):
         raise NotImplementedError
 
     @property
-    def valid_values(self) -> TList[Any]:
+    def valid_values(self) -> Tuple:
         return self._valid_values
 
 
@@ -95,7 +95,7 @@ class Anything(Validator):
     """allow any value to pass"""
 
     def __init__(self) -> None:
-        self._valid_values = [0]
+        self._valid_values = (0,)
 
     def validate(self, value: Any, context: str=''):
         pass
@@ -130,7 +130,7 @@ class Bool(Validator):
     """
 
     def __init__(self) -> None:
-        self._valid_values = [True, False]
+        self._valid_values = (True, False)
 
     def validate(self, value: bool, context: str='') -> None:
         if not isinstance(value, bool) and not isinstance(value, np.bool8):
@@ -158,7 +158,7 @@ def __init__(self, min_length: int=0, max_length: int=BIGSTRING) -> None:
         else:
             raise TypeError('max_length must be a positive integer '
                             'no smaller than min_length')
-        self._valid_values = ['.'*min_length]
+        self._valid_values = ('.'*min_length,)
 
     def validate(self, value: str, context: str=''):
         if not isinstance(value, str):
@@ -207,7 +207,7 @@ def __init__(self, min_value: numbertypes=-float("inf"),
         else:
             raise TypeError('max_value must be a number bigger than min_value')
 
-        self._valid_values = [min_value, max_value]
+        self._valid_values = (min_value, max_value)
 
     def validate(self, value: numbertypes, context: str='') -> None:
         if not isinstance(value, self.validtypes):
@@ -253,7 +253,7 @@ def __init__(self, min_value: inttypes=-BIGINT,
             raise TypeError(
                 'max_value must be an integer bigger than min_value')
 
-        self._valid_values = [min_value, max_value]
+        self._valid_values = (min_value, max_value)
 
     def validate(self, value: inttypes, context: str='') -> None:
         if not isinstance(value, self.validtypes):
@@ -308,7 +308,7 @@ def __init__(self, *values: Hashable) -> None:
             raise TypeError('Enum needs at least one value')
 
         self._values = set(values)
-        self._valid_values = list(values)
+        self._valid_values = tuple(values)
 
     def validate(self, value, context: str='') -> None:
         try:
@@ -358,7 +358,7 @@ def __init__(self, divisor: int=1, **kwargs) -> None:
         if not isinstance(divisor, int) or divisor <= 0:
             raise TypeError('divisor must be a positive integer')
         self._divisor = divisor
-        self._valid_values = [divisor]
+        self._valid_values = (divisor,)
 
     def validate(self, value: int, context: str='') -> None:
         super().validate(value=value, context=context)
@@ -401,7 +401,7 @@ def __init__(self, divisor: numbertypes,
             self._mulval: Optional[Multiples] = Multiples(divisor=abs(divisor))
         else:
             self._mulval = None
-        self._valid_values = [divisor]
+        self._valid_values = (divisor,)
 
     def validate(self, value: numbertypes,
                  context: str='') -> None:
@@ -461,10 +461,8 @@ def __init__(self, *validators: Validator) -> None:
                 self.is_numeric = True
 
         self._validators = tuple(validators)
-        self._valid_values = []
-        for val in self._validators:
-            self._valid_values += val._valid_values
-        self._valid_values = list(set(self._valid_values))
+        self._valid_values = tuple(vval for v in self._validators
+                                   for vval in v._valid_values)
 
     def validate(self, value: Any, context: str='') -> None:
         args: TList[str] = []
@@ -514,14 +512,14 @@ def __init__(self, min_value: numbertypes=-float("inf"),
         self._shape = shape
 
     @property
-    def valid_values(self) -> TList[np.ndarray]:
+    def valid_values(self) -> Tuple[np.ndarray]:
         shape = self.shape
         if shape is None:
-            return [np.array([self._min_value])]
+            return (np.array([self._min_value]),)
         else:
             val_arr = np.empty(shape)
             val_arr.fill(self._min_value)
-            return [val_arr]
+            return (val_arr,)
 
     @property
     def shape(self) -> Optional[Tuple[int, ...]]:
@@ -588,7 +586,7 @@ class Lists(Validator):
 
     def __init__(self, elt_validator: Validator=Anything()) -> None:
         self._elt_validator = elt_validator
-        self._valid_values = [elt_validator._valid_values]
+        self._valid_values = ([vval for vval in elt_validator._valid_values],)
 
     def __repr__(self) -> str:
         msg = '<Lists : '
@@ -617,7 +615,7 @@ def __init__(self, elt_validator: Validator=Anything(),
         self._elt_validator = elt_validator
         self._length = length
         self._require_sorted = require_sorted
-        self._valid_values = [elt_validator._valid_values]
+        self._valid_values = ([vval for vval in elt_validator._valid_values],)
 
     def __repr__(self) -> str:
         msg = '<Sequence : '
@@ -647,7 +645,7 @@ class Callable(Validator):
     Validator for callables such as functions.
     """
     def __init__(self) -> None:
-        self._valid_values = [lambda: 0]
+        self._valid_values = (lambda: 0,)
 
     def validate(self, value: TCallable, context: str='') -> None:
         if not callable(value):
@@ -670,7 +668,7 @@ def __init__(self, allowed_keys: TSequence[Hashable]=None) -> None:
             allowed_keys (List): if set, all keys must be in allowed_keys
         """
         self.allowed_keys = allowed_keys
-        self._valid_values = [{0: 1}]
+        self._valid_values =({0: 1},)
 
     def validate(self, value: dict, context: str='') -> None:
         if not isinstance(value, dict):


Test patch:
diff --git a/qcodes/tests/test_validators.py b/qcodes/tests/test_validators.py
--- a/qcodes/tests/test_validators.py
+++ b/qcodes/tests/test_validators.py
@@ -74,7 +74,8 @@ def test_bool(self):
 
     def test_valid_values(self):
         val = Bool()
-        val.validate(val.valid_values[0])
+        for vval in val.valid_values:
+            val.validate(vval)
 
 
 class TestStrings(TestCase):
@@ -175,7 +176,8 @@ def test_failed_strings(self):
 
     def test_valid_values(self):
         val = Strings()
-        val.validate(val.valid_values[0])
+        for vval in val.valid_values:
+            val.validate(vval)
 
 
 class TestNumbers(TestCase):
@@ -278,6 +280,10 @@ def test_failed_numbers(self):
             with self.assertRaises(TypeError):
                 Numbers(min_value=val)
 
+    def test_valid_values(self):
+        val = Numbers()
+        for vval in val.valid_values:
+            val.validate(vval)
 
 class TestInts(TestCase):
     ints = [0, 1, 10, -1, 100, 1000000, int(-1e15), int(1e15),
@@ -361,6 +367,11 @@ def test_failed_numbers(self):
             with self.assertRaises((TypeError, OverflowError)):
                 Ints(min_value=val)
 
+    def test_valid_values(self):
+        val = Ints()
+        for vval in val.valid_values:
+            val.validate(vval)
+
 
 class TestPermissiveInts(TestCase):
 
@@ -389,6 +400,11 @@ def test_bad_values(self):
                     validator.validate(i)
 
 
+    def test_valid_values(self):
+        val = PermissiveInts()
+        for vval in val.valid_values:
+            val.validate(vval)
+
 class TestEnum(TestCase):
     enums = [
         [True, False],
@@ -474,7 +490,8 @@ def test_valid_values(self):
 
         for d in self.divisors:
             n = Multiples(divisor=d)
-            n.validate(n._valid_values[0])
+            for num in n.valid_values:
+                n.validate(num)
 
 
 class TestPermissiveMultiples(TestCase):
@@ -540,15 +557,19 @@ def test_good(self):
 
         self.assertFalse(MultiType(Strings(), Enum(1, 2)).is_numeric)
 
+
     def test_bad(self):
         for args in [[], [1], [Strings(), True]]:
             with self.assertRaises(TypeError):
                 MultiType(*args)
 
     def test_valid_values(self):
-        m = MultiType(Strings(2, 4), Ints(10, 32))
-        for val in m.valid_values:
-            m.validate(val)
+        ms = [MultiType(Strings(2, 4), Ints(10, 32)),
+              MultiType(Ints(), Lists(Ints())),
+              MultiType(Ints(), MultiType(Lists(Ints()), Ints()))]
+        for m in ms:
+            for val in m.valid_values:
+                m.validate(val)
 
 
 class TestArrays(TestCase):
@@ -599,7 +620,8 @@ def test_shape_defered(self):
 
     def test_valid_values(self):
         val = Arrays(min_value=-5, max_value=50, shape=(2, 2))
-        val.validate(val.valid_values[0])
+        for vval in val.valid_values:
+            val.validate(vval)
 
 
 class TestLists(TestCase):
@@ -623,8 +645,9 @@ def test_elt_vals(self):
             l.validate(v2)
 
     def test_valid_values(self):
-        l = Lists(Ints(max_value=10))
-        l.validate(l.valid_values[0])
+        val = Lists(Ints(max_value=10))
+        for vval in val.valid_values:
+            val.validate(vval)
 
 
 class TestCallable(TestCase):
@@ -640,8 +663,9 @@ def test_func():
             c.validate(test_int)
 
     def test_valid_values(self):
-        c = Callable()
-        c.validate(c.valid_values[0])
+        val = Callable()
+        for vval in val.valid_values:
+            val.validate(vval)
 
 
 class TestDict(TestCase):
@@ -655,5 +679,6 @@ def test_dict(self):
             d.validate(test_int)
 
     def test_valid_values(self):
-        d = Dict()
-        d.validate(d.valid_values[0])
+        val = Dict()
+        for vval in val.valid_values:
+            val.validate(vval)