diff --git a/jsonpatch.py b/jsonpatch.py index 8e67b4c..5383aec 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -669,9 +669,9 @@ def _move_key(self, value): try: # Serialized like in _differing_runs return self.dumps(_sorted_members(value)) - except (TypeError, ValueError): - # Values that cannot be serialized are only moved where they are - # added unchanged + except (TypeError, ValueError, RecursionError): + # Values that cannot be serialized, e.g. as they contain + # themselves, are only moved where they are added unchanged return id(value) def store_index(self, value, index, st): @@ -838,7 +838,7 @@ def _differing_runs(self, src, dst): # but independent of the order of object members src_keys = [self.dumps(_sorted_members(item)) for item in src] dst_keys = [self.dumps(_sorted_members(item)) for item in dst] - except (TypeError, ValueError): + except (TypeError, ValueError, RecursionError): # Items that cannot be serialized are compared by position return [(0, len(src), 0, len(dst))] diff --git a/property_tests.py b/property_tests.py index 87c1848..9e92ebf 100644 --- a/property_tests.py +++ b/property_tests.py @@ -292,6 +292,9 @@ def test_roundtrip(self, docs): self.check_roundtrip(*docs) @given(safe_doc_pairs) + # the diff considered e.g. 1 and true equal, #180 + @example(docs=([0], [False])) + @example(docs=({'a': [1]}, {'b': [True]})) def test_roundtrip_of_safe_documents(self, docs): self.check_roundtrip(*docs) diff --git a/tests.py b/tests.py index 8744e2d..af42fbc 100755 --- a/tests.py +++ b/tests.py @@ -647,6 +647,12 @@ def test_move_only_values_equal_in_json(self): ([[1], [2], [3]], [[2], [3], [True]]), ([{'a': 1}, 2], [2, {'a': True}]), ({'a': [1]}, {'b': [True]}), + ({'a': {'x': 1}}, {'b': {'x': True}}), + ([[1], 0], [0, [True]]), + ({'a': [1]}, {'b': [1.0]}), + ({'a': 0.0}, {'b': -0.0}), + # member names that are not strings are serialized, too + ({'a': {1: 'v'}}, {'b': {True: 'v'}}), ] for src, dst in cases: with self.subTest(src=src, dst=dst): @@ -654,6 +660,15 @@ def test_move_only_values_equal_in_json(self): res = patch.apply(src) self.assertEqual(json.dumps(res), json.dumps(dst)) + def test_move_only_values_equal_in_given_dumps(self): + """The given dumps decides which values are the same""" + src = {'a': decimal.Decimal('1.0')} + dst = {'b': decimal.Decimal('1.00')} + patch = jsonpatch.JsonPatch.from_diff( + src, dst, dumps=custom_types_dumps) + res = jsonpatch.apply_patch(src, patch) + self.assertEqual(str(res['b']), '1.00') + def test_values_dumps_cannot_serialize(self): """Such values can be added, removed and moved, as they need not be compared""" @@ -671,6 +686,18 @@ def test_values_dumps_cannot_serialize(self): self.assertEqual(jsonpatch.make_patch(*cases[-1]).patch, [{'op': 'move', 'from': '/a', 'path': '/b'}]) + def test_values_that_contain_themselves(self): + """dumps cannot serialize them either, but they can still be added, + removed and moved""" + value = [] + value.append(value) + self.assertEqual(jsonpatch.make_patch({}, {'a': value}).patch, + [{'op': 'add', 'path': '/a', 'value': value}]) + self.assertEqual(jsonpatch.make_patch({'a': [value]}, {'a': []}).patch, + [{'op': 'remove', 'path': '/a/0'}]) + self.assertEqual(jsonpatch.make_patch({'a': value}, {'b': value}).patch, + [{'op': 'move', 'from': '/a', 'path': '/b'}]) + def test_issue119(self): """Make sure it avoids casting numeric str dict key to int""" src = [ @@ -900,6 +927,12 @@ def fn(_src, _dst): fn({'foo': [1, 2, 3]}, {'foo': [3, 2, 1]}) fn([1, 2, 3], [3, 2, 1]) + def test_use_move_regardless_of_member_order(self): + src = {'a': {'x': 1, 'y': [True, {}]}} + dst = {'b': {'y': [True, {}], 'x': 1}} + patch = list(jsonpatch.make_patch(src, dst)) + self.assertEqual(patch, [{'op': 'move', 'from': '/a', 'path': '/b'}]) + def test_success_if_replace_inside_dict(self): src = [{'a': 1, 'foo': {'b': 2, 'd': 5}}] dst = [{'a': 1, 'foo': {'b': 3, 'd': 6}}]