Repository: thisfred/val

Issue:
Return all validation errors, rather than just the first one.
Currently val's ValidationErrors only contain a single validation error message, even if the data was invalid in more than one way. This makes validation a tiny bit faster, but at the expense of not being as informative as it could be. I think I am going to reverse this decision.

Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/2031381/badge)](https://coveralls.io/builds/2031381)

Coverage remained the same at 100.0% when pulling **8f5f8492be58f84c36c8751ec81a90b9d4f64353 on collect-all-errors** into **a60e8de415d9ed855570fc09ee14a5974532cf07 on master**.


Patch:
diff --git a/README.rst b/README.rst
index bc413c0..33d435d 100644
--- a/README.rst
+++ b/README.rst
@@ -184,11 +184,10 @@ not missing any of the keys specified (unless they are specified as
     >>> schema.validates({'foo': 12, 'bar': 888, 'baz': 299})
     True
 
-    >>> schema.validate({'foo': 'bar'})
+    >>> schema.validate({'foo': 'bar', 'baz': 'qux'})
     Traceback (most recent call last):
         ...
-    val.NotValid: 'foo': 'bar' is not of type <class 'int'>
-
+    val.NotValid: ("'foo': 'bar' is not of type <class 'int'>", "'baz': 'qux' not matched")
     >>> schema.validate({'qux': 19})
     Traceback (most recent call last):
         ...
@@ -286,7 +285,7 @@ elements passed into the Or:
     >>> schema.validate('bar')
     Traceback (most recent call last):
         ...
-    val.NotValid: 'bar' is not equal to 'foo', 'bar' is not of type <class 'int'>
+    val.NotValid: 'bar' is not equal to 'foo' and 'bar' is not of type <class 'int'>
 
 
 And()
diff --git a/pp.yaml b/pp.yaml
index f3a43d0..bdcd9e8 100644
--- a/pp.yaml
+++ b/pp.yaml
@@ -10,8 +10,6 @@ ignore-patterns:
 
 pep8:
   run: true
-  options:
-    max-line-length: 80
 
 mccabe:
   run: true
@@ -44,3 +42,5 @@ pylint:
   disable:
     - too-few-public-methods
     - invalid-name
+    - star-args
+    - line-too-long
diff --git a/requirements.txt b/requirements.txt
index b8313ef..20b74fc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,1 @@
-teleport
 pyRFC3339>=0.2
diff --git a/val/__init__.py b/val/__init__.py
index b22cb10..42fd75a 100644
--- a/val/__init__.py
+++ b/val/__init__.py
@@ -60,7 +60,7 @@ def build_callable_validator(function):
                 return data
 
         except (TypeError, ValueError, NotValid) as ex:
-            raise NotValid(', '.join(ex.args))
+            raise NotValid(ex.args)
 
         raise NotValid("%r invalidated by '%s'" % (data, get_repr(function)))
 
@@ -116,14 +116,17 @@ def _determine_keys(dictionary):
 
 def _validate_mandatory_keys(mandatory, validated, data, to_validate):
     """Validate the manditory keys."""
+    errors = []
     for key, sub_schema in mandatory.items():
         if key not in data:
-            raise NotValid('missing key: %r' % (key,))
+            errors.append('missing key: %r' % (key,))
+            continue
         try:
             validated[key] = sub_schema(data[key])
         except NotValid as ex:
-            raise NotValid('%r: %s' % (key, ', '.join(ex.args)))
+            errors.extend(['%r: %s' % (key, arg) for arg in ex.args])
         to_validate.remove(key)
+    return errors
 
 
 def _validate_optional_key(key, missing, value, validated, optional):
@@ -131,9 +134,10 @@ def _validate_optional_key(key, missing, value, validated, optional):
     try:
         validated[key] = optional[key](value)
     except NotValid as ex:
-        raise NotValid('%r: %s' % (key, ', '.join(ex.args)))
+        return ['%r: %s' % (key, arg) for arg in ex.args]
     if key in missing:
         missing.remove(key)
+    return []
 
 
 def _validate_type_key(key, value, types, validated):
@@ -146,20 +150,24 @@ def _validate_type_key(key, value, types, validated):
         except NotValid:
             continue
         else:
-            break
-    else:
-        raise NotValid('%r: %r not matched' % (key, value))
+            return []
+
+    return ['%r: %r not matched' % (key, value)]
 
 
 def _validate_other_keys(optional, types, missing, validated, data,
                          to_validate):
     """Validate the rest of the keys present in the data."""
+    errors = []
     for key in to_validate:
         value = data[key]
         if key in optional:
-            _validate_optional_key(key, missing, value, validated, optional)
+            errors.extend(
+                _validate_optional_key(
+                    key, missing, value, validated, optional))
             continue
-        _validate_type_key(key, value, types, validated)
+        errors.extend(_validate_type_key(key, value, types, validated))
+    return errors
 
 
 def build_dict_validator(dictionary):
@@ -174,9 +182,13 @@ def build_dict_validator(dictionary):
 
         validated = {}
         to_validate = list(data.keys())
-        _validate_mandatory_keys(mandatory, validated, data, to_validate)
-        _validate_other_keys(
-            optional, types, missing, validated, data, to_validate)
+        errors = _validate_mandatory_keys(
+            mandatory, validated, data, to_validate)
+        errors.extend(
+            _validate_other_keys(
+                optional, types, missing, validated, data, to_validate))
+        if errors:
+            raise NotValid(*errors)
         for key in missing:
             validated[key] = defaults[key][0]
 
@@ -233,11 +245,14 @@ class BaseSchema(object):
     def validate(self, data):
         """Validate data. Raise NotValid error for invalid data."""
         validated = self._validated(data)
+        errors = []
         for validator in self.additional_validators:
             if not validator(validated):
-                raise NotValid(
+                errors.append(
                     "%s invalidated by '%s'" % (
                         validated, get_repr(validator)))
+        if errors:
+            raise NotValid(*errors)
 
         if self.default is UNSPECIFIED:
             return validated
@@ -303,7 +318,7 @@ class Or(BaseSchema):
             except NotValid as ex:
                 errors.extend(ex.args)
 
-        raise NotValid(', '.join(errors))
+        raise NotValid(' and '.join(errors))
 
     def __repr__(self):
         return "<%s>" % (" or ".join(["%r" % (v,) for v in self.values]),)
@@ -347,7 +362,7 @@ class Convert(BaseSchema):
         try:
             return self.convert(data)
         except (TypeError, ValueError) as ex:
-            raise NotValid(', '.join(ex.args))
+            raise NotValid(*ex.args)
 
     def __repr__(self):
         """Display schema."""


Test patch:
diff --git a/tests/test_val.py b/tests/test_val.py
index c60bf63..fa4825b 100644
--- a/tests/test_val.py
+++ b/tests/test_val.py
@@ -487,3 +487,10 @@ def test_cannot_change_definition():
     schema = Schema({"foo": "bar"})
     with pytest.raises(AttributeError):
         schema.definition = {"qux": "baz"}
+
+
+def test_captures_multiple_errors():
+    schema = Schema({"foo": str})
+    with pytest.raises(NotValid) as exception:
+        schema.validate({'foo': 12, 'bar': 'qux'})
+    assert len(exception.value.args) == 2