summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Smith <smithdc@gmail.com>2020-07-21 20:05:33 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-07-29 12:04:13 +0200
commit95da207bdb7550922781bbdb3a7b993db0dfc4cf (patch)
treec48490f9c8f42d096df4c29a4cc3bfaf25a4a895
parent16218c20606d8cd89c5393970c83da04598a3e04 (diff)
Fixed #28507 -- Made ValidationError.__eq__() ignore messages and params ordering.
Co-authored-by: caleb logan <clogan202@gmail.com>
-rw-r--r--django/core/exceptions.py20
-rw-r--r--docs/releases/3.2.txt3
-rw-r--r--tests/test_exceptions/test_validation_error.py269
3 files changed, 292 insertions, 0 deletions
diff --git a/django/core/exceptions.py b/django/core/exceptions.py
index dc084b8692..2b3e55fe49 100644
--- a/django/core/exceptions.py
+++ b/django/core/exceptions.py
@@ -1,6 +1,9 @@
"""
Global Django exception and warning classes.
"""
+import operator
+
+from django.utils.hashable import make_hashable
class FieldDoesNotExist(Exception):
@@ -182,6 +185,23 @@ class ValidationError(Exception):
def __repr__(self):
return 'ValidationError(%s)' % self
+ def __eq__(self, other):
+ if not isinstance(other, ValidationError):
+ return NotImplemented
+ 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,
+ ))
+ if hasattr(self, 'error_dict'):
+ return hash(tuple(sorted(make_hashable(self.error_dict))))
+ return hash(tuple(sorted(self.error_list, key=operator.attrgetter('message'))))
+
class EmptyResultSet(Exception):
"""A database query predicate is impossible."""
diff --git a/docs/releases/3.2.txt b/docs/releases/3.2.txt
index 10468ac655..129026cf89 100644
--- a/docs/releases/3.2.txt
+++ b/docs/releases/3.2.txt
@@ -341,6 +341,9 @@ Validators
of a raised :exc:`~django.core.exceptions.ValidationError`. This allows
custom error messages to use the ``%(value)s`` placeholder.
+* The :class:`.ValidationError` equality operator now ignores ``messages`` and
+ ``params`` ordering.
+
.. _backwards-incompatible-3.2:
Backwards incompatible changes in 3.2
diff --git a/tests/test_exceptions/test_validation_error.py b/tests/test_exceptions/test_validation_error.py
index a0bbfaf77e..12aa8e2775 100644
--- a/tests/test_exceptions/test_validation_error.py
+++ b/tests/test_exceptions/test_validation_error.py
@@ -1,4 +1,5 @@
import unittest
+from unittest import mock
from django.core.exceptions import ValidationError
@@ -14,3 +15,271 @@ class TestValidationError(unittest.TestCase):
message_dict['field2'] = ['E3', 'E4']
exception = ValidationError(message_dict)
self.assertEqual(sorted(exception.messages), ['E1', 'E2', 'E3', 'E4'])
+
+ def test_eq(self):
+ error1 = ValidationError('message')
+ error2 = ValidationError('message', code='my_code1')
+ error3 = ValidationError('message', code='my_code2')
+ error4 = ValidationError(
+ 'error %(parm1)s %(parm2)s',
+ code='my_code1',
+ params={'parm1': 'val1', 'parm2': 'val2'},
+ )
+ error5 = ValidationError({'field1': 'message', 'field2': 'other'})
+ error6 = ValidationError({'field1': 'message'})
+ error7 = ValidationError([
+ ValidationError({'field1': 'field error', 'field2': 'other'}),
+ 'message',
+ ])
+
+ self.assertEqual(error1, ValidationError('message'))
+ self.assertNotEqual(error1, ValidationError('message2'))
+ self.assertNotEqual(error1, error2)
+ self.assertNotEqual(error1, error4)
+ self.assertNotEqual(error1, error5)
+ self.assertNotEqual(error1, error6)
+ self.assertNotEqual(error1, error7)
+ self.assertEqual(error1, mock.ANY)
+ self.assertEqual(error2, ValidationError('message', code='my_code1'))
+ self.assertNotEqual(error2, ValidationError('other', code='my_code1'))
+ self.assertNotEqual(error2, error3)
+ self.assertNotEqual(error2, error4)
+ self.assertNotEqual(error2, error5)
+ self.assertNotEqual(error2, error6)
+ self.assertNotEqual(error2, error7)
+
+ self.assertEqual(error4, ValidationError(
+ 'error %(parm1)s %(parm2)s',
+ code='my_code1',
+ params={'parm1': 'val1', 'parm2': 'val2'},
+ ))
+ self.assertNotEqual(error4, ValidationError(
+ 'error %(parm1)s %(parm2)s',
+ code='my_code2',
+ params={'parm1': 'val1', 'parm2': 'val2'},
+ ))
+ self.assertNotEqual(error4, ValidationError(
+ 'error %(parm1)s %(parm2)s',
+ code='my_code1',
+ params={'parm2': 'val2'},
+ ))
+ self.assertNotEqual(error4, ValidationError(
+ 'error %(parm1)s %(parm2)s',
+ code='my_code1',
+ params={'parm2': 'val1', 'parm1': 'val2'},
+ ))
+ self.assertNotEqual(error4, ValidationError(
+ 'error val1 val2',
+ code='my_code1',
+ ))
+ # params ordering is ignored.
+ self.assertEqual(error4, ValidationError(
+ 'error %(parm1)s %(parm2)s',
+ code='my_code1',
+ params={'parm2': 'val2', 'parm1': 'val1'},
+ ))
+
+ self.assertEqual(
+ error5,
+ ValidationError({'field1': 'message', 'field2': 'other'}),
+ )
+ self.assertNotEqual(
+ error5,
+ ValidationError({'field1': 'message', 'field2': 'other2'}),
+ )
+ self.assertNotEqual(
+ error5,
+ ValidationError({'field1': 'message', 'field3': 'other'}),
+ )
+ self.assertNotEqual(error5, error6)
+ # fields ordering is ignored.
+ self.assertEqual(
+ error5,
+ ValidationError({'field2': 'other', 'field1': 'message'}),
+ )
+
+ self.assertNotEqual(error7, ValidationError(error7.error_list[1:]))
+ self.assertNotEqual(
+ ValidationError(['message']),
+ ValidationError([ValidationError('message', code='my_code')]),
+ )
+ # messages ordering is ignored.
+ self.assertEqual(
+ error7,
+ ValidationError(list(reversed(error7.error_list))),
+ )
+
+ self.assertNotEqual(error4, ValidationError([error4]))
+ self.assertNotEqual(ValidationError([error4]), error4)
+ self.assertNotEqual(error4, ValidationError({'field1': error4}))
+ self.assertNotEqual(ValidationError({'field1': error4}), error4)
+
+ def test_eq_nested(self):
+ error_dict = {
+ 'field1': ValidationError(
+ 'error %(parm1)s %(parm2)s',
+ code='my_code',
+ params={'parm1': 'val1', 'parm2': 'val2'},
+ ),
+ 'field2': 'other',
+ }
+ error = ValidationError(error_dict)
+ self.assertEqual(error, ValidationError(dict(error_dict)))
+ self.assertEqual(error, ValidationError({
+ 'field1': ValidationError(
+ 'error %(parm1)s %(parm2)s',
+ code='my_code',
+ params={'parm2': 'val2', 'parm1': 'val1'},
+ ),
+ 'field2': 'other',
+ }))
+ self.assertNotEqual(error, ValidationError(
+ {**error_dict, 'field2': 'message'},
+ ))
+ self.assertNotEqual(error, ValidationError({
+ 'field1': ValidationError(
+ 'error %(parm1)s val2',
+ code='my_code',
+ params={'parm1': 'val1'},
+ ),
+ 'field2': 'other',
+ }))
+
+ def test_hash(self):
+ error1 = ValidationError('message')
+ error2 = ValidationError('message', code='my_code1')
+ error3 = ValidationError('message', code='my_code2')
+ error4 = ValidationError(
+ 'error %(parm1)s %(parm2)s',
+ code='my_code1',
+ params={'parm1': 'val1', 'parm2': 'val2'},
+ )
+ error5 = ValidationError({'field1': 'message', 'field2': 'other'})
+ error6 = ValidationError({'field1': 'message'})
+ error7 = ValidationError([
+ ValidationError({'field1': 'field error', 'field2': 'other'}),
+ 'message',
+ ])
+
+ self.assertEqual(hash(error1), hash(ValidationError('message')))
+ self.assertNotEqual(hash(error1), hash(ValidationError('message2')))
+ self.assertNotEqual(hash(error1), hash(error2))
+ self.assertNotEqual(hash(error1), hash(error4))
+ self.assertNotEqual(hash(error1), hash(error5))
+ self.assertNotEqual(hash(error1), hash(error6))
+ self.assertNotEqual(hash(error1), hash(error7))
+ self.assertEqual(
+ hash(error2),
+ hash(ValidationError('message', code='my_code1')),
+ )
+ self.assertNotEqual(
+ hash(error2),
+ hash(ValidationError('other', code='my_code1')),
+ )
+ self.assertNotEqual(hash(error2), hash(error3))
+ self.assertNotEqual(hash(error2), hash(error4))
+ self.assertNotEqual(hash(error2), hash(error5))
+ self.assertNotEqual(hash(error2), hash(error6))
+ self.assertNotEqual(hash(error2), hash(error7))
+
+ self.assertEqual(hash(error4), hash(ValidationError(
+ 'error %(parm1)s %(parm2)s',
+ code='my_code1',
+ params={'parm1': 'val1', 'parm2': 'val2'},
+ )))
+ self.assertNotEqual(hash(error4), hash(ValidationError(
+ 'error %(parm1)s %(parm2)s',
+ code='my_code2',
+ params={'parm1': 'val1', 'parm2': 'val2'},
+ )))
+ self.assertNotEqual(hash(error4), hash(ValidationError(
+ 'error %(parm1)s %(parm2)s',
+ code='my_code1',
+ params={'parm2': 'val2'},
+ )))
+ self.assertNotEqual(hash(error4), hash(ValidationError(
+ 'error %(parm1)s %(parm2)s',
+ code='my_code1',
+ params={'parm2': 'val1', 'parm1': 'val2'},
+ )))
+ self.assertNotEqual(hash(error4), hash(ValidationError(
+ 'error val1 val2',
+ code='my_code1',
+ )))
+ # params ordering is ignored.
+ self.assertEqual(hash(error4), hash(ValidationError(
+ 'error %(parm1)s %(parm2)s',
+ code='my_code1',
+ params={'parm2': 'val2', 'parm1': 'val1'},
+ )))
+
+ self.assertEqual(
+ hash(error5),
+ hash(ValidationError({'field1': 'message', 'field2': 'other'})),
+ )
+ self.assertNotEqual(
+ hash(error5),
+ hash(ValidationError({'field1': 'message', 'field2': 'other2'})),
+ )
+ self.assertNotEqual(
+ hash(error5),
+ hash(ValidationError({'field1': 'message', 'field3': 'other'})),
+ )
+ self.assertNotEqual(error5, error6)
+ # fields ordering is ignored.
+ self.assertEqual(
+ hash(error5),
+ hash(ValidationError({'field2': 'other', 'field1': 'message'})),
+ )
+
+ self.assertNotEqual(
+ hash(error7),
+ hash(ValidationError(error7.error_list[1:])),
+ )
+ self.assertNotEqual(
+ hash(ValidationError(['message'])),
+ hash(ValidationError([ValidationError('message', code='my_code')])),
+ )
+ # messages ordering is ignored.
+ self.assertEqual(
+ hash(error7),
+ hash(ValidationError(list(reversed(error7.error_list)))),
+ )
+
+ self.assertNotEqual(hash(error4), hash(ValidationError([error4])))
+ self.assertNotEqual(hash(ValidationError([error4])), hash(error4))
+ self.assertNotEqual(
+ hash(error4),
+ hash(ValidationError({'field1': error4})),
+ )
+
+ def test_hash_nested(self):
+ error_dict = {
+ 'field1': ValidationError(
+ 'error %(parm1)s %(parm2)s',
+ code='my_code',
+ params={'parm2': 'val2', 'parm1': 'val1'},
+ ),
+ 'field2': 'other',
+ }
+ error = ValidationError(error_dict)
+ self.assertEqual(hash(error), hash(ValidationError(dict(error_dict))))
+ self.assertEqual(hash(error), hash(ValidationError({
+ 'field1': ValidationError(
+ 'error %(parm1)s %(parm2)s',
+ code='my_code',
+ params={'parm1': 'val1', 'parm2': 'val2'},
+ ),
+ 'field2': 'other',
+ })))
+ self.assertNotEqual(hash(error), hash(ValidationError(
+ {**error_dict, 'field2': 'message'},
+ )))
+ self.assertNotEqual(hash(error), hash(ValidationError({
+ 'field1': ValidationError(
+ 'error %(parm1)s val2',
+ code='my_code',
+ params={'parm1': 'val1'},
+ ),
+ 'field2': 'other',
+ })))