Repository: encode/typesystem

Issue:
ValidationError can't be shown with `traceback.print_exc()` in Python 3.6
Consider the following code:

```python
import traceback
import typesystem

try:
    typesystem.Integer().validate("hello")
except typesystem.ValidationError:
    traceback.print_exc()
```

Because of an issue with Python 3.6 discussed in [issue 28803](https://bugs.python.org/issue28603), since `ValidationError` implements `__eq__` but not `__hash__` it is not hashable and `print_exc()` fails to process it correctly.

```
TypeError: unhashable type: 'ValidationError'
```

(Full traceback available [here](https://travis-ci.org/bocadilloproject/bocadillo/jobs/513086149))

Looks like we'd need to implement `ValidationError.__hash__()` to fix this.

Discussion:

Patch:
diff --git a/typesystem/base.py b/typesystem/base.py
index bd15068..def115c 100644
--- a/typesystem/base.py
+++ b/typesystem/base.py
@@ -78,6 +78,10 @@ class Message:
             and self.end_position == other.end_position
         )
 
+    def __hash__(self) -> int:
+        ident = (self.code, tuple(self.index))
+        return hash(ident)
+
     def __repr__(self) -> str:
         class_name = self.__class__.__name__
         index_str = f", index={self.index!r}" if self.index else ""
@@ -183,6 +187,10 @@ class BaseError(Mapping, Exception):
     def __eq__(self, other: typing.Any) -> bool:
         return isinstance(other, ValidationError) and self._messages == other._messages
 
+    def __hash__(self) -> int:
+        ident = tuple(hash(m) for m in self._messages)
+        return hash(ident)
+
     def __repr__(self) -> str:
         class_name = self.__class__.__name__
         if len(self._messages) == 1 and not self._messages[0].index:


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
index b908a66..56f24e6 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -825,3 +825,9 @@ def test_error_messages_interface():
     validator = Integer()
     value, error = validator.validate_or_error("abc")
     assert error.messages() == [Message(text="Must be a number.", code="type")]
+
+
+def test_validation_error_is_hashable():
+    validator = Integer()
+    _, error = validator.validate_or_error("abc")
+    hash(error)