diff --git a/CHANGELOG.md b/CHANGELOG.md index de391252afd079f9cde739653362ec67423e8f6b..3656f0ce0677644108e382c170eb7d2923e792e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ # v2.17.12 +* added deep comparison diff --git a/docs/coding/functions.rst b/docs/coding/functions.rst index 828ab5fddd37d18bcbb07520b01fa0be210abcff..a929934062f5d55757d6e714f87cf04da7ce9e93 100644 --- a/docs/coding/functions.rst +++ b/docs/coding/functions.rst @@ -110,3 +110,16 @@ Without running into `TypeError: metaclass conflict: the metaclass of a derived Following function will help with that: .. autofunction:: satella.coding.metaclass_maker + +Deep comparison +--------------- + +To analyze why two objects don't compare the same, you can use the following functions: + +.. autofunction:: satella.coding.assert_equal + +.. autoclass:: satella.coding.Inequal + :members: + +.. autoclass:: satella.coding.InequalityReason + :members: diff --git a/satella/__init__.py b/satella/__init__.py index 18ad53ff5ab5f25beda8c3983a04ea4f9465bbcf..154feec1b6f40b8d9ae8fb7374a4d4d3c0596361 100644 --- a/satella/__init__.py +++ b/satella/__init__.py @@ -1 +1 @@ -__version__ = '2.17.12a1' +__version__ = '2.17.12a2' diff --git a/satella/coding/__init__.py b/satella/coding/__init__.py index d30cbf4308887def9778fd7c804695170ee2e77c..1d58a54bb1864415f7401fb26b4b3b241c3680f3 100644 --- a/satella/coding/__init__.py +++ b/satella/coding/__init__.py @@ -20,8 +20,10 @@ from .overloading import overload, class_or_instancemethod from .recast_exceptions import rethrow_as, silence_excs, catch_exception, log_exceptions, \ raises_exception, reraise_as from .expect_exception import expect_exception +from .deep_compare import assert_equal, InequalityReason, Inequal __all__ = [ + 'assert_equal', 'InequalityReason', 'Inequal', 'Closeable', 'contains', 'enum_value', 'reraise_as', 'expect_exception', 'overload', 'class_or_instancemethod', diff --git a/satella/coding/deep_compare.py b/satella/coding/deep_compare.py new file mode 100644 index 0000000000000000000000000000000000000000..530b70d8979b366ea48b27b129a79c200af442a3 --- /dev/null +++ b/satella/coding/deep_compare.py @@ -0,0 +1,63 @@ +import enum + + +class InequalityReason(enum.IntEnum): + NOT_EQUAL = 0 #: direct eq yielded not equal + LENGTH_MISMATCH = 1 #: length didn't match + KEY_NOT_FOUND = 2 #: key given as obj1 was not found + + +class Inequal(Exception): + """ + An exception raised by :meth:`~satella.coding.deep_compare` if two objects don't match + + :ivar obj1: first object that was not equal, or key name + :ivar obj2: second object that was not equal, or None + :ivar reason: (:class:`~satella.coding.InequalityReason`) reason for inequality + """ + def __init__(self, obj1, obj2, reason: InequalityReason): + self.obj1 = obj1 + self.obj2 = obj2 + self.reason = reason + + +def assert_equal(a, b): + """ + Assert that two values are equal. If not, an :class:`satella.coding.Inequality` exception + will be thrown. + + Objects are tried to compare using it's :code:`__eq__`. + + :param a: first value to compare + :param b: second value to compare + :raises Inequal: objects were not equal + """ + if isinstance(a, (int, float, str, bytes, bytearray, type(None))): + if a != b: + raise Inequal(a, b, InequalityReason.NOT_EQUAL) + return + elif isinstance(a, (set, frozenset)): + if len(a) != len(b): + raise Inequal(a, b, InequalityReason.LENGTH_MISMATCH) + if a != b: + raise Inequal(a, b, InequalityReason.LENGTH_MISMATCH) + elif isinstance(a, (list, tuple)): + if len(a) != len(b): + raise Inequal(a, b, InequalityReason.LENGTH_MISMATCH) + for c, d in zip(a, b): + assert_equal(c, d) + elif isinstance(a, dict): + if len(a) != len(b): + raise Inequal(a, b, InequalityReason.LENGTH_MISMATCH) + for key in a.keys(): + if key not in b: + raise Inequal(key, None, InequalityReason.KEY_NOT_FOUND) + assert_equal(a[key], b[key]) + else: + try: + if a == b: + return + raise Inequal(a, b, InequalityReason.NOT_EQUAL) + except TypeError: + pass + diff --git a/tests/test_coding/test_deep_compare.py b/tests/test_coding/test_deep_compare.py new file mode 100644 index 0000000000000000000000000000000000000000..5d046c3478d971306f35383f7efd3211efd4b328 --- /dev/null +++ b/tests/test_coding/test_deep_compare.py @@ -0,0 +1,28 @@ +import unittest + +from satella.coding import assert_equal, Inequal + + +class TestDeepCompare(unittest.TestCase): + def assertInequal(self, a, b): + self.assertRaises(Inequal, lambda: assert_equal(a, b)) + + def test_compare(self): + assert_equal(2, 2) + self.assertInequal(3, 2) + self.assertInequal([], [6]) + self.assertInequal({1: 2}, {3: 4}) + self.assertInequal({1: 2}, {1: 4}) + self.assertInequal({1: 2}, {3: 4, 5: 6}) + assert_equal([1], [1]) + assert_equal(set([1, 2]), set([2, 1])) + + class Object: + def __init__(self, a): + self.a = a + + def __eq__(self, other): + return self.a == other.a + + assert_equal(Object(3), Object(3)) + self.assertInequal(Object(3), Object(4))