diff --git a/jsonpatch.py b/jsonpatch.py index e704ab7..3756f36 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -869,6 +869,9 @@ def from_diff( one. The resulting patch transforms `src` into `dst`, so it has to be applied to `src` (or to a document equal to it). + Values in the patch are copied, so changing either input document + afterwards does not change the generated patch. + :param src: Source JSON document, including arrays and scalar values. :type src: object @@ -906,7 +909,7 @@ def from_diff( optimization=optimization, generate_test_ops=generate_test_ops) builder._compare_values((), None, src, dst) - ops = list(builder.execute()) + ops = copy.deepcopy(list(builder.execute())) return cls(ops, pointer_cls=pointer_cls) def to_string(self, dumps=None): diff --git a/tests.py b/tests.py index 5cd2ca2..a4de557 100755 --- a/tests.py +++ b/tests.py @@ -609,6 +609,36 @@ def json_loader(obj): class MakePatchTestCase(unittest.TestCase): + def test_generated_values_do_not_alias_target(self): + documents = [ + ({}, {'a': {'nested': [1]}}), + ({'a': 0}, {'a': {'nested': [1]}}), + ([], [{'nested': [1]}]), + ([0], [{'nested': [1]}]), + (None, {'a': {'nested': [1]}}), + ] + for src, dst in documents: + for optimization in (True, False): + with self.subTest(src=src, optimization=optimization): + target = copy.deepcopy(dst) + patch = jsonpatch.JsonPatch.from_diff( + src, target, optimization=optimization) + serialized = patch.to_string() + value = target[0] if isinstance(target, list) else target['a'] + value['nested'].append(2) + + self.assertEqual(patch.to_string(), serialized) + self.assertEqual(patch.apply(src), dst) + + def test_editing_generated_values_does_not_change_target(self): + for src in ({}, {'a': 0}): + with self.subTest(src=src): + dst = {'a': {'nested': [1]}} + patch = jsonpatch.make_patch(src, dst) + patch.patch[0]['value']['nested'].append(2) + self.assertEqual(dst, {'a': {'nested': [1]}}) + self.assertEqual(patch.apply(src), {'a': {'nested': [1, 2]}}) + def test_apply_patch_to_copy(self): src = {'foo': 'bar', 'boo': 'qux'} dst = {'baz': 'qux', 'foo': 'boo'} @@ -874,8 +904,13 @@ def test_values_that_contain_themselves(self): removed and moved""" value = [] value.append(value) - self.assertEqual(jsonpatch.make_patch({}, {'a': value}).patch, - [{'op': 'add', 'path': '/a', 'value': value}]) + patch = jsonpatch.make_patch({}, {'a': value}).patch + self.assertEqual(len(patch), 1) + self.assertEqual(patch[0]['op'], 'add') + self.assertEqual(patch[0]['path'], '/a') + copied = patch[0]['value'] + self.assertIsNot(copied, value) + self.assertIs(copied[0], copied) self.assertEqual(jsonpatch.make_patch({'a': [value]}, {'a': []}).patch, [{'op': 'remove', 'path': '/a/0'}]) self.assertEqual(jsonpatch.make_patch({'a': value}, {'b': value}).patch,