Repository: alecthomas/voluptuous

Issue:
list validation swallows more useful/explicit error messages
Take this validator and schema:

```python
def is_even(value):
    if value % 2:
        raise Invalid('%i is not even' % value)
    return value

schema = Schema(dict(even_number=All(int, is_even)))
schema(dict(even_number=3))
```

We get a useful error message:

```
voluptuous.MultipleInvalid: 3 is not even for dictionary value @ data['even_number']
```

Sadly, as soon as we want a list of these, we lose the useful error message:

```python
schema = Schema(dict(even_numbers=[All(int, is_even)]))
schema(dict(even_numbers=[3]))
````

Instead, we get a much less useful message:

```
voluptuous.MultipleInvalid: invalid list value @ data['even_numbers'][0]
```

Sadly, nowhere in that `MultipleInvalid` exception is the original error stored.

Discussion:

Patch:
diff --git a/README.md b/README.md
index d991125..74490c2 100644
--- a/README.md
+++ b/README.md
@@ -522,7 +522,7 @@ backtracking is attempted:
 ...   raise AssertionError('MultipleInvalid not raised')
 ... except MultipleInvalid as e:
 ...   exc = e
->>> str(exc) == "invalid list value @ data[0][0]"
+>>> str(exc) == "not a valid value @ data[0][0]"
 True
 
 ```
diff --git a/voluptuous.py b/voluptuous.py
index 3ab4de4..4b1215e 100644
--- a/voluptuous.py
+++ b/voluptuous.py
@@ -227,10 +227,6 @@ class InclusiveInvalid(Invalid):
     """Not all values found in inclusion group."""
 
 
-class SequenceItemInvalid(Invalid):
-    """One of the values found in a sequence was invalid."""
-
-
 class SequenceTypeInvalid(Invalid):
     """The type found is not a sequence type."""
 
@@ -664,7 +660,7 @@ class Schema(object):
         >>> validator = Schema(['one', 'two', int])
         >>> validator(['one'])
         ['one']
-        >>> with raises(MultipleInvalid, 'invalid list value @ data[0]'):
+        >>> with raises(MultipleInvalid, 'expected int @ data[0]'):
         ...   validator([3.5])
         >>> validator([1])
         [1]
@@ -698,8 +694,6 @@ class Schema(object):
                             raise
                         invalid = e
                 else:
-                    if len(invalid.path) <= len(index_path):
-                        invalid = SequenceItemInvalid('invalid %s value' % seq_type_name, index_path)
                     errors.append(invalid)
             if errors:
                 raise MultipleInvalid(errors)
@@ -714,7 +708,7 @@ class Schema(object):
         >>> validator = Schema(('one', 'two', int))
         >>> validator(('one',))
         ('one',)
-        >>> with raises(MultipleInvalid, 'invalid tuple value @ data[0]'):
+        >>> with raises(MultipleInvalid, 'expected int @ data[0]'):
         ...   validator((3.5,))
         >>> validator((1,))
         (1,)
@@ -729,7 +723,7 @@ class Schema(object):
         >>> validator = Schema(['one', 'two', int])
         >>> validator(['one'])
         ['one']
-        >>> with raises(MultipleInvalid, 'invalid list value @ data[0]'):
+        >>> with raises(MultipleInvalid, 'expected int @ data[0]'):
         ...   validator([3.5])
         >>> validator([1])
         [1]
@@ -1095,7 +1089,7 @@ class Msg(object):
     Messages are only applied to invalid direct descendants of the schema:
 
     >>> validate = Schema(Msg([['one', 'two', int]], 'not okay!'))
-    >>> with raises(MultipleInvalid, 'invalid list value @ data[0][0]'):
+    >>> with raises(MultipleInvalid, 'expected int @ data[0][0]'):
     ...   validate([['three']])
 
     The type which is thrown can be overridden but needs to be a subclass of Invalid


Test patch:
diff --git a/tests.md b/tests.md
index f098c1b..18f6fba 100644
--- a/tests.md
+++ b/tests.md
@@ -16,7 +16,7 @@ value:
     ...   raise AssertionError('MultipleInvalid not raised')
     ... except MultipleInvalid as e:
     ...   exc = e
-    >>> str(exc) == 'invalid list value @ data[1]'
+    >>> str(exc) == 'expected a dictionary @ data[1]'
     True
 
 It should also be accurate for nested values:
@@ -35,7 +35,7 @@ It should also be accurate for nested values:
     ... except MultipleInvalid as e:
     ...   exc = e
     >>> str(exc)
-    "invalid list value @ data[0]['four'][0]"
+    "not a valid value @ data[0]['four'][0]"
 
     >>> try:
     ...   schema([{'six': {'seven': 'nine'}}])
@@ -116,9 +116,9 @@ Multiple errors are reported:
     ...   schema([1, 2, 3])
     ... except MultipleInvalid as e:
     ...   print([str(i) for i in e.errors])  # doctest: +NORMALIZE_WHITESPACE
-    ['invalid list value @ data[0]',
-     'invalid list value @ data[1]',
-     'invalid list value @ data[2]']
+    ['expected a list @ data[0]',
+     'expected a list @ data[1]',
+     'expected a list @ data[2]']
 
 Required fields in dictionary which are invalid should not have required :
 
diff --git a/tests.py b/tests.py
index 0dd4e9d..32e6694 100644
--- a/tests.py
+++ b/tests.py
@@ -112,7 +112,7 @@ def test_literal():
     try:
         schema([{"c": 1}])
     except Invalid as e:
-        assert_equal(str(e), 'invalid list value @ data[0]')
+        assert_equal(str(e), "{'c': 1} not match for {'b': 1} @ data[0]")
     else:
         assert False, "Did not raise Invalid"
 
@@ -242,3 +242,23 @@ def test_repr():
     )
     assert_equal(repr(coerce_), "Coerce(int, msg='moo')")
     assert_equal(repr(all_), "All('10', Coerce(int, msg=None), msg='all msg')")
+
+
+def test_list_validation_messages():
+    """ Make sure useful error messages are available """
+
+    def is_even(value):
+        if value % 2:
+            raise Invalid('%i is not even' % value)
+        return value
+
+    schema = Schema(dict(even_numbers=[All(int, is_even)]))
+
+    try:
+        schema(dict(even_numbers=[3]))
+    except Invalid as e:
+        assert_equal(len(e.errors), 1, e.errors)
+        assert_equal(str(e.errors[0]), "3 is not even @ data['even_numbers'][0]")
+        assert_equal(str(e), "3 is not even @ data['even_numbers'][0]")
+    else:
+        assert False, "Did not raise Invalid"