diff --git a/django/core/exceptions.py b/django/core/exceptions.py index 5780ffdb4a..673d004d57 100644 --- a/django/core/exceptions.py +++ b/django/core/exceptions.py @@ -196,15 +196,14 @@ class ValidationError(Exception): return hash(self) == hash(other) def __hash__(self): - # Ignore params and messages ordering. if hasattr(self, 'message'): return hash(( self.message, self.code, - tuple(sorted(make_hashable(self.params))) if self.params else None, + make_hashable(self.params), )) if hasattr(self, 'error_dict'): - return hash(tuple(sorted(make_hashable(self.error_dict)))) + return hash(make_hashable(self.error_dict)) return hash(tuple(sorted(self.error_list, key=operator.attrgetter('message')))) diff --git a/django/utils/hashable.py b/django/utils/hashable.py index 0bef5b003d..7d137ccc2f 100644 --- a/django/utils/hashable.py +++ b/django/utils/hashable.py @@ -2,10 +2,15 @@ from django.utils.itercompat import is_iterable def make_hashable(value): + """ + Attempt to make value hashable or raise a TypeError if it fails. + + The returned value should generate the same hash for equal values. + """ if isinstance(value, dict): return tuple([ (key, make_hashable(nested_value)) - for key, nested_value in value.items() + for key, nested_value in sorted(value.items()) ]) # Try hash to avoid converting a hashable iterable (e.g. string, frozenset) # to a tuple. diff --git a/tests/utils_tests/test_hashable.py b/tests/utils_tests/test_hashable.py index b4db3ef7d7..d267b112cc 100644 --- a/tests/utils_tests/test_hashable.py +++ b/tests/utils_tests/test_hashable.py @@ -10,7 +10,8 @@ class TestHashable(SimpleTestCase): ({}, ()), ({'a'}, ('a',)), (frozenset({'a'}), {'a'}), - ({'a': 1}, (('a', 1),)), + ({'a': 1, 'b': 2}, (('a', 1), ('b', 2))), + ({'b': 2, 'a': 1}, (('a', 1), ('b', 2))), (('a', ['b', 1]), ('a', ('b', 1))), (('a', {'b': 1}), ('a', (('b', 1),))), )