diff --git a/jsonpatch.py b/jsonpatch.py index 5383aec..4f830eb 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -195,7 +195,8 @@ def __hash__(self): def __eq__(self, other): if not isinstance(other, PatchOperation): return False - return self.operation == other.operation + # e.g. tests for 1 and for true behave differently + return _json_equal(self.operation, other.operation) def __ne__(self, other): return not(self == other) @@ -410,7 +411,7 @@ def apply(self, obj): raise InvalidJsonPatch( "The operation does not contain a 'value' member") - if val != value: + if not _json_equal(val, value): msg = '{0} ({1}) is not equal to tested value {2} ({3})' raise JsonPatchTestFailed(msg.format(val, type(val), value, type(value))) @@ -961,6 +962,25 @@ def _positional_cost(old, new): return 1 +def _json_equal(first, second): + """ Compares values the way RFC 6902, 4.6 does: numbers by value, but + literals like true only with themselves, which Python considers equal to + 1, and arrays and objects member by member """ + if isinstance(first, bool) or isinstance(second, bool): + return isinstance(first, bool) and isinstance(second, bool) and \ + first == second + if isinstance(first, MutableMapping) and \ + isinstance(second, MutableMapping): + return len(first) == len(second) and all( + key in second and _json_equal(first[key], second[key]) + for key in first) + if isinstance(first, MutableSequence) and \ + isinstance(second, MutableSequence): + return len(first) == len(second) and all( + _json_equal(*items) for items in zip(first, second)) + return first == second + + def _sorted_members(value): """ Copies the arrays and objects in value, with the members of objects sorted by key """ diff --git a/property_tests.py b/property_tests.py index 9e92ebf..dc6b8b1 100644 --- a/property_tests.py +++ b/property_tests.py @@ -427,11 +427,13 @@ def test_replace_any_existing_location(self, case, value): doc, [{'op': 'replace', 'path': to_pointer(parts), 'value': value}]) assert_json_equal(resolve(result, parts), value) - # RFC 6902, 4.6: literals like true are only equal to themselves, but - # 'test' uses Python equality, which considers 1 and True equal - @unittest.expectedFailure + # RFC 6902, 4.6: literals like true are only equal to themselves, although + # Python considers 1 and True equal, #216 @given(doc_pairs) @example(values=(1, True)) + @example(values=([1], [True])) + @example(values=({'a': 0}, {'a': False})) + @example(values=(1, 1.0)) def test_test_operation_uses_json_equality(self, values): value, tested = values result = outcome(jsonpatch.apply_patch, {'a': value}, diff --git a/tests.py b/tests.py index af42fbc..40ea99b 100755 --- a/tests.py +++ b/tests.py @@ -288,6 +288,23 @@ def test_test_error(self): jsonpatch.apply_patch, obj, [{'op': 'test', 'path': '/bar', 'value': 'bar'}]) + def test_test_numbers_by_value(self): + obj = {'a': 1, 'b': [0.0], 'c': {'d': 2}} + jsonpatch.apply_patch(obj, [{'op': 'test', 'path': '/a', 'value': 1.0}, + {'op': 'test', 'path': '/b', 'value': [0]}, + {'op': 'test', 'path': '/c', 'value': {'d': 2.0}}]) + + def test_test_literals_differ_from_numbers(self): + # RFC 6902, 4.6: true and false are only equal to themselves, #216 + for doc, value in [({'a': 1}, True), + ({'a': [1]}, [True]), + ({'a': {'a': 0}}, {'a': False}), + ({'a': True}, 1), + ({'a': False}, 0)]: + with self.assertRaises(jsonpatch.JsonPatchTestFailed): + jsonpatch.apply_patch( + doc, [{'op': 'test', 'path': '/a', 'value': value}]) + def test_test_not_existing(self): obj = {'bar': 'qux'} @@ -363,6 +380,17 @@ def test_patch_unequal(self): patch2 = jsonpatch.JsonPatch([{'op': 'test', 'path': '/test1'}]) self.assertNotEqual(patch1, patch2) + def test_patch_unequal_literal_and_number(self): + # the patches behave differently, #216 + patch1 = jsonpatch.JsonPatch([{'op': 'test', 'path': '/a', 'value': 1}]) + patch2 = jsonpatch.JsonPatch([{'op': 'test', 'path': '/a', 'value': True}]) + self.assertNotEqual(patch1, patch2) + + def test_patch_equal_numbers_by_value(self): + patch1 = jsonpatch.JsonPatch([{'op': 'test', 'path': '/a', 'value': [1]}]) + patch2 = jsonpatch.JsonPatch([{'op': 'test', 'path': '/a', 'value': [1.0]}]) + self.assertEqual(patch1, patch2) + def test_patch_hash_equality(self): patch1 = jsonpatch.JsonPatch([{ "op": "add", "path": "/a/b/c", "value": "foo" }]) patch2 = jsonpatch.JsonPatch([{ "path": "/a/b/c", "op": "add", "value": "foo" }])