From 2cb66b9007fab6250f8898b3c0a95e1b04402184 Mon Sep 17 00:00:00 2001 From: Robert Cochran Date: Tue, 16 Jan 2024 14:47:30 -0800 Subject: [PATCH 1/7] Add `generate_test_ops` flag --- bin/jsondiff | 4 +++- jsonpatch.py | 48 +++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/bin/jsondiff b/bin/jsondiff index 5ac0090..495d71a 100755 --- a/bin/jsondiff +++ b/bin/jsondiff @@ -18,6 +18,8 @@ parser.add_argument('-u', '--preserve-unicode', action='store_true', help='Output Unicode character as-is without using Code Point') parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + jsonpatch.__version__) +parser.add_argument('-t', '--test-ops', action='store_true', + help='Generate before-state test ops for remove/replace') def main(): @@ -32,7 +34,7 @@ def diff_files(): args = parser.parse_args() doc1 = json.load(args.FILE1) doc2 = json.load(args.FILE2) - patch = jsonpatch.make_patch(doc1, doc2) + patch = jsonpatch.make_patch(doc1, doc2, generate_test_ops=args.test_ops) if patch.patch: print(json.dumps(patch.patch, indent=args.indent, ensure_ascii=not(args.preserve_unicode))) sys.exit(1) diff --git a/jsonpatch.py b/jsonpatch.py index d3fc26d..5d0342f 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -157,7 +157,7 @@ def apply_patch(doc, patch, in_place=False, pointer_cls=JsonPointer): return patch.apply(doc, in_place) -def make_patch(src, dst, pointer_cls=JsonPointer): +def make_patch(src, dst, generate_test_ops=False, pointer_cls=JsonPointer): """Generates patch by comparing two document objects. Actually is a proxy to :meth:`JsonPatch.from_diff` method. @@ -170,6 +170,12 @@ def make_patch(src, dst, pointer_cls=JsonPointer): :param pointer_cls: JSON pointer class to use. :type pointer_cls: Type[JsonPointer] + :param generate_test_ops: While :const:`True` generate test operations + capturing previous values of `replace`/`remove` + operations. By default do not generate these + operations. + :type generate_test_ops: bool + >>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]} >>> dst = {'baz': 'qux', 'numbers': [1, 4, 7]} >>> patch = make_patch(src, dst) @@ -178,7 +184,7 @@ def make_patch(src, dst, pointer_cls=JsonPointer): True """ - return JsonPatch.from_diff(src, dst, pointer_cls=pointer_cls) + return JsonPatch.from_diff(src, dst, generate_test_ops=generate_test_ops, pointer_cls=pointer_cls) class PatchOperation(object): @@ -475,6 +481,11 @@ def apply(self, obj): return obj + def _on_undo_add(self, path, key): + return key + + def _on_undo_remove(self, path, key): + return key class CopyOperation(PatchOperation): """ Copies an object property or an array element to a new location """ @@ -628,7 +639,7 @@ def from_string(cls, patch_str, loads=None, pointer_cls=JsonPointer): @classmethod def from_diff( - cls, src, dst, optimization=True, dumps=None, + cls, src, dst, generate_test_ops=False, dumps=None, pointer_cls=JsonPointer, ): """Creates JsonPatch instance based on comparison of two document @@ -641,6 +652,12 @@ def from_diff( :param dst: Data source document object. :type dst: dict + :param generate_test_ops: While :const:`True` generate test operations + capturing previous values of `replace`/`remove` + operations. By default do not generate these + operations. + :type generate_test_ops: bool + :param dumps: A function of one argument that produces a serialized JSON string. :type dumps: function @@ -658,7 +675,7 @@ def from_diff( True """ json_dumper = dumps or cls.json_dumper - builder = DiffBuilder(src, dst, json_dumper, pointer_cls=pointer_cls) + builder = DiffBuilder(src, dst, json_dumper, generate_test_ops=generate_test_ops, pointer_cls=pointer_cls) builder._compare_values('', None, src, dst) ops = list(builder.execute()) return cls(ops, pointer_cls=pointer_cls) @@ -711,9 +728,10 @@ def _get_operation(self, operation): class DiffBuilder(object): - def __init__(self, src_doc, dst_doc, dumps=json.dumps, pointer_cls=JsonPointer): + def __init__(self, src_doc, dst_doc, dumps=json.dumps, generate_test_ops=False, pointer_cls=JsonPointer): self.dumps = dumps self.pointer_cls = pointer_cls + self.generate_test_ops = generate_test_ops self.index_storage = [{}, {}] self.index_storage2 = [[], []] self.__root = root = [] @@ -819,6 +837,13 @@ def _item_added(self, path, key, item): self.store_index(item, new_index, _ST_ADD) def _item_removed(self, path, key, item): + if self.generate_test_ops: + test_index = self.insert(TestOperation({ + 'op': 'test', + 'path': _path_join(path, key), + 'value': item, + }, pointer_cls=self.pointer_cls)) + new_op = RemoveOperation({ 'op': 'remove', 'path': _path_join(path, key), @@ -847,11 +872,20 @@ def _item_removed(self, path, key, item): else: self.remove(new_index) + if self.generate_test_ops: + self.remove(test_index) else: self.store_index(item, new_index, _ST_REMOVE) - def _item_replaced(self, path, key, item): + def _item_replaced(self, path, key, item, old_item): + if self.generate_test_ops: + self.insert(TestOperation({ + 'op': 'test', + 'path': _path_join(path, key), + 'value': old_item, + }, pointer_cls=self.pointer_cls)) + self.insert(ReplaceOperation({ 'op': 'replace', 'path': _path_join(path, key), @@ -921,7 +955,7 @@ def _compare_values(self, path, key, src, dst): return else: - self._item_replaced(path, key, dst) + self._item_replaced(path, key, dst, src) def _path_join(path, key): From 597b76e1868618cf2d528305bb840b640c84bc80 Mon Sep 17 00:00:00 2001 From: Robert Cochran Date: Tue, 16 Jan 2024 15:12:10 -0800 Subject: [PATCH 2/7] Extend test suite --- tests.py | 66 +++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/tests.py b/tests.py index d9eea92..b29bb98 100755 --- a/tests.py +++ b/tests.py @@ -325,32 +325,33 @@ def json_loader(obj): class MakePatchTestCase(unittest.TestCase): + generate_test_ops = False def test_apply_patch_to_copy(self): src = {'foo': 'bar', 'boo': 'qux'} dst = {'baz': 'qux', 'foo': 'boo'} - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) res = patch.apply(src) self.assertTrue(src is not res) def test_apply_patch_to_same_instance(self): src = {'foo': 'bar', 'boo': 'qux'} dst = {'baz': 'qux', 'foo': 'boo'} - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) res = patch.apply(src, in_place=True) self.assertTrue(src is res) def test_objects(self): src = {'foo': 'bar', 'boo': 'qux'} dst = {'baz': 'qux', 'foo': 'boo'} - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) res = patch.apply(src) self.assertEqual(res, dst) def test_arrays(self): src = {'numbers': [1, 2, 3], 'other': [1, 3, 4, 5]} dst = {'numbers': [1, 3, 4, 5], 'other': [1, 3, 4]} - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) res = patch.apply(src) self.assertEqual(res, dst) @@ -361,7 +362,7 @@ def test_complex_object(self): dst = {'data': [ {'foo': [42]}, {'bar': []}, {'baz': {'boo': 'oom!'}} ]} - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) res = patch.apply(src) self.assertEqual(res, dst) @@ -369,7 +370,7 @@ def test_array_add_remove(self): # see https://github.com/stefankoegl/python-json-patch/issues/4 src = {'numbers': [], 'other': [1, 5, 3, 4]} dst = {'numbers': [1, 3, 4, 5], 'other': []} - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) res = patch.apply(src) self.assertEqual(res, dst) @@ -388,7 +389,7 @@ def test_add_nested(self): def _test_should_just_add_new_item_not_rebuild_all_list(self): src = {'foo': [1, 2, 3]} dst = {'foo': [3, 1, 2, 3]} - patch = list(jsonpatch.make_patch(src, dst)) + patch = list(jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops)) self.assertEqual(len(patch), 1) self.assertEqual(patch[0]['op'], 'add') res = jsonpatch.apply_patch(src, patch) @@ -397,8 +398,14 @@ def _test_should_just_add_new_item_not_rebuild_all_list(self): def test_escape(self): src = {"x/y": 1} dst = {"x/y": 2} - patch = jsonpatch.make_patch(src, dst) - self.assertEqual([{"path": "/x~1y", "value": 2, "op": "replace"}], patch.patch) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) + + if self.generate_test_ops: + self.assertEqual([{"path": "/x~1y", "value": 1, "op": "test"}, + {"path": "/x~1y", "value": 2, "op": "replace"}], patch.patch) + else: + self.assertEqual([{"path": "/x~1y", "value": 2, "op": "replace"}], patch.patch) + res = patch.apply(src) self.assertEqual(res, dst) @@ -406,7 +413,7 @@ def test_root_list(self): """ Test making and applying a patch of the root is a list """ src = [{'foo': 'bar', 'boo': 'qux'}] dst = [{'baz': 'qux', 'foo': 'boo'}] - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) res = patch.apply(src) self.assertEqual(res, dst) @@ -414,7 +421,7 @@ def test_make_patch_unicode(self): """ Test if unicode keys and values are handled correctly """ src = {} dst = {'\xee': '\xee'} - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) res = patch.apply(src) self.assertEqual(res, dst) @@ -423,15 +430,21 @@ def test_issue40(self): src = [8, 7, 2, 1, 0, 9, 4, 3, 5, 6] dest = [7, 2, 1, 0, 9, 4, 3, 6, 5, 8] - jsonpatch.make_patch(src, dest) + jsonpatch.make_patch(src, dest, generate_test_ops=self.generate_test_ops) def test_issue76(self): """ Make sure op:remove does not include a 'value' field """ src = { "name": "fred", "friend": "barney", "spouse": "wilma" } dst = { "name": "fred", "spouse": "wilma" } - expected = [{"path": "/friend", "op": "remove"}] - patch = jsonpatch.make_patch(src, dst) + + if self.generate_test_ops: + expected = [{"path": "/friend", "op": "test", "value": "barney"}, + {"path": "/friend", "op": "remove"}] + else: + expected = [{"path": "/friend", "op": "remove"}] + + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) self.assertEqual(patch.patch, expected) res = jsonpatch.apply_patch(src, patch) self.assertEqual(res, dst) @@ -443,7 +456,7 @@ def test_json_patch(self): new = { 'queue': {'teams_out': [{'id': 5, 'reason': 'If lose'}]} } - patch = jsonpatch.make_patch(old, new) + patch = jsonpatch.make_patch(old, new, generate_test_ops=self.generate_test_ops) new_from_patch = jsonpatch.apply_patch(old, patch) self.assertEqual(new, new_from_patch) @@ -452,7 +465,7 @@ def test_arrays_one_element_sequences(self): # see https://github.com/stefankoegl/python-json-patch/issues/30#issuecomment-155070128 src = [1,2,3] dst = [3,1,4,2] - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) res = jsonpatch.apply_patch(src, patch) self.assertEqual(res, dst) @@ -462,7 +475,7 @@ def test_list_in_dict(self): https://github.com/stefankoegl/python-json-patch/issues/74 """ old = {'key': [{'someNumber': 0, 'someArray': [1, 2, 3]}]} new = {'key': [{'someNumber': 0, 'someArray': [1, 2, 3, 4]}]} - patch = jsonpatch.make_patch(old, new) + patch = jsonpatch.make_patch(old, new, generate_test_ops=self.generate_test_ops) new_from_patch = jsonpatch.apply_patch(old, patch) self.assertEqual(new, new_from_patch) @@ -472,7 +485,7 @@ def test_nested(self): https://github.com/stefankoegl/python-json-patch/issues/41 """ old = {'school':{'names':['Kevin','Carl']}} new = {'school':{'names':['Carl','Kate','Kevin','Jake']}} - patch = jsonpatch.JsonPatch.from_diff(old, new) + patch = jsonpatch.JsonPatch.from_diff(old, new, generate_test_ops=self.generate_test_ops) new_from_patch = jsonpatch.apply_patch(old, patch) self.assertEqual(new, new_from_patch) @@ -480,7 +493,7 @@ def test_move_from_numeric_to_alpha_dict_key(self): #https://github.com/stefankoegl/python-json-patch/issues/97 src = {'13': 'x'} dst = {'A': 'a', 'b': 'x'} - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) res = jsonpatch.apply_patch(src, patch) self.assertEqual(res, dst) @@ -488,7 +501,7 @@ def test_issue90(self): """In JSON 1 is different from True even though in python 1 == True""" src = {'A': 1} dst = {'A': True} - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) res = jsonpatch.apply_patch(src, patch) self.assertEqual(res, dst) self.assertIsInstance(res['A'], bool) @@ -497,7 +510,7 @@ def test_issue129(self): """In JSON 1 is different from True even though in python 1 == True Take Two""" src = {'A': {'D': 1.0}, 'B': {'E': 'a'}} dst = {'A': {'C': 'a'}, 'B': {'C': True}} - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) res = jsonpatch.apply_patch(src, patch) self.assertEqual(res, dst) self.assertIsInstance(res['B']['C'], bool) @@ -506,7 +519,7 @@ def test_issue103(self): """In JSON 1 is different from 1.0 even though in python 1 == 1.0""" src = {'A': 1} dst = {'A': 1.0} - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) res = jsonpatch.apply_patch(src, patch) self.assertEqual(res, dst) self.assertIsInstance(res['A'], float) @@ -522,7 +535,7 @@ def test_issue119(self): {'foobar': {u'1': [u'lettuce', u'cabbage', u'bok choy', u'broccoli'], u'3': [u'ibex'], u'2': [u'apple'], u'5': [], u'4': [u'gerenuk', u'duiker'], u'10_1576156603109': [], u'6': [], u'8_1572034252560': [u'thompson', u'gravie', u'mango', u'coconut'], u'7_1572034204585': []}}, {'foobar': {u'description': u'', u'title': u''}} ] - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) res = jsonpatch.apply_patch(src, patch) self.assertEqual(res, dst) @@ -562,7 +575,7 @@ def test_issue120(self): '16': 'service', '2': ['zero', 'enable']}} ] - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) res = jsonpatch.apply_patch(src, patch) self.assertEqual(res, dst) @@ -597,6 +610,10 @@ def test_custom_types_subclass_load(self): self.assertEqual(new, new_from_patch) +class MakePatchWithTestOpsTestCase(MakePatchTestCase): + generate_test_ops = True + + class OptimizationTests(unittest.TestCase): def test_use_replace_instead_of_remove_add(self): src = {'foo': [1, 2, 3]} @@ -1051,6 +1068,7 @@ def get_suite(): suite.addTest(unittest.makeSuite(ApplyPatchTestCase)) suite.addTest(unittest.makeSuite(EqualityTestCase)) suite.addTest(unittest.makeSuite(MakePatchTestCase)) + suite.addTest(unittest.makeSuite(MakePatchWithTestOpsTestCase)) suite.addTest(unittest.makeSuite(ListTests)) suite.addTest(unittest.makeSuite(InvalidInputTests)) suite.addTest(unittest.makeSuite(ConflictTests)) From 13e9ab04a9a45c08e5548a2e22833d7c2f128350 Mon Sep 17 00:00:00 2001 From: Robert Cochran Date: Mon, 29 Jan 2024 15:44:19 -0800 Subject: [PATCH 3/7] Do not print error warning over pandoc --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 7753be1..d0ac8e5 100644 --- a/setup.py +++ b/setup.py @@ -42,8 +42,8 @@ from pypandoc import convert read_md = lambda f: convert(f, 'rst') except ImportError: - print('warning: pypandoc module not found, could not convert ' - 'Markdown to RST') + #print('warning: pypandoc module not found, could not convert ' + # 'Markdown to RST') read_md = lambda f: open(f, 'r').read() CLASSIFIERS = [ From 35734fbfa7b86de21d52a395007bc81ff41eda15 Mon Sep 17 00:00:00 2001 From: Robert Cochran Date: Tue, 30 Jan 2024 13:29:23 -0800 Subject: [PATCH 4/7] Revert "Do not print error warning over pandoc" This reverts commit 13e9ab04a9a45c08e5548a2e22833d7c2f128350. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index d0ac8e5..7753be1 100644 --- a/setup.py +++ b/setup.py @@ -42,8 +42,8 @@ from pypandoc import convert read_md = lambda f: convert(f, 'rst') except ImportError: - #print('warning: pypandoc module not found, could not convert ' - # 'Markdown to RST') + print('warning: pypandoc module not found, could not convert ' + 'Markdown to RST') read_md = lambda f: open(f, 'r').read() CLASSIFIERS = [ From e1de7e46783b776b99f2afb872b5ec423c7c8b3a Mon Sep 17 00:00:00 2001 From: Robert Cochran Date: Fri, 22 Mar 2024 15:20:28 -0700 Subject: [PATCH 5/7] Remove generated test op when transforming remove to move --- jsonpatch.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/jsonpatch.py b/jsonpatch.py index 5d0342f..840cbe0 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -819,6 +819,11 @@ def _item_added(self, path, key, item): for v in self.iter_from(index): op.key = v._on_undo_remove(op.path, op.key) + if self.generate_test_ops: + prev_op_index = index[0] + if isinstance(prev_op_index[2], TestOperation): + self.remove(prev_op_index) + self.remove(index) if op.location != _path_join(path, key): new_op = MoveOperation({ From 821ed0a51b28d96dac79548fcf00562f2cfb2f71 Mon Sep 17 00:00:00 2001 From: Robert Cochran Date: Fri, 22 Mar 2024 15:21:32 -0700 Subject: [PATCH 6/7] Extend OptimizationTests to be dual-tested with generate_test_ops --- tests.py | 47 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/tests.py b/tests.py index b29bb98..1ec8341 100755 --- a/tests.py +++ b/tests.py @@ -615,21 +615,34 @@ class MakePatchWithTestOpsTestCase(MakePatchTestCase): class OptimizationTests(unittest.TestCase): + generate_test_ops = False + def test_use_replace_instead_of_remove_add(self): src = {'foo': [1, 2, 3]} dst = {'foo': [3, 2, 3]} - patch = list(jsonpatch.make_patch(src, dst)) - self.assertEqual(len(patch), 1) - self.assertEqual(patch[0]['op'], 'replace') + patch = list(jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops)) + + if self.generate_test_ops: + self.assertEqual(len(patch), 2) + self.assertEqual(patch[0]['op'], 'test') + self.assertEqual(patch[1]['op'], 'replace') + else: + self.assertEqual(len(patch), 1) + self.assertEqual(patch[0]['op'], 'replace') + res = jsonpatch.apply_patch(src, patch) self.assertEqual(res, dst) def test_use_replace_instead_of_remove_add_nested(self): src = {'foo': [{'bar': 1, 'baz': 2}, {'bar': 2, 'baz': 3}]} dst = {'foo': [{'bar': 1}, {'bar': 2, 'baz': 3}]} - patch = list(jsonpatch.make_patch(src, dst)) + patch = list(jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops)) exp = [{'op': 'remove', 'path': '/foo/0/baz'}] + + if self.generate_test_ops: + exp.insert(0, {'path': '/foo/0/baz', 'value': 2, 'op': 'test'}) + self.assertEqual(patch, exp) res = jsonpatch.apply_patch(src, patch) @@ -638,7 +651,7 @@ def test_use_replace_instead_of_remove_add_nested(self): def test_use_move_instead_of_remove_add(self): src = {'foo': [4, 1, 2, 3]} dst = {'foo': [1, 2, 3, 4]} - patch = list(jsonpatch.make_patch(src, dst)) + patch = list(jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops)) self.assertEqual(len(patch), 1) self.assertEqual(patch[0]['op'], 'move') res = jsonpatch.apply_patch(src, patch) @@ -646,7 +659,7 @@ def test_use_move_instead_of_remove_add(self): def test_use_move_instead_of_add_remove(self): def fn(_src, _dst): - patch = list(jsonpatch.make_patch(_src, _dst)) + patch = list(jsonpatch.make_patch(_src, _dst, generate_test_ops=self.generate_test_ops)) # Check if there are only 'move' operations for p in patch: self.assertEqual(p['op'], 'move') @@ -661,25 +674,25 @@ def fn(_src, _dst): def test_success_if_replace_inside_dict(self): src = [{'a': 1, 'foo': {'b': 2, 'd': 5}}] dst = [{'a': 1, 'foo': {'b': 3, 'd': 6}}] - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) self.assertEqual(patch.apply(src), dst) def test_success_if_replace_single_value(self): src = [{'a': 1, 'b': 2, 'd': 5}] dst = [{'a': 1, 'c': 3, 'd': 5}] - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) self.assertEqual(patch.apply(src), dst) def test_success_if_replaced_by_object(self): src = [{'a': 1, 'b': 2, 'd': 5}] dst = [{'d': 6}] - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) self.assertEqual(patch.apply(src), dst) def test_success_if_correct_patch_appied(self): src = [{'a': 1}, {'b': 2}] dst = [{'a': 1, 'b': 2}] - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) self.assertEqual(patch.apply(src), dst) def test_success_if_correct_expected_patch_appied(self): @@ -689,7 +702,9 @@ def test_success_if_correct_expected_patch_appied(self): {'path': '/0/a', 'op': 'remove'}, {'path': '/0/c', 'op': 'add', 'value': 2} ] - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) + if self.generate_test_ops: + exp.insert(0, {'path': '/0/a', 'value': 1, 'op': 'test'}) self.assertEqual(patch.patch, exp) # verify that this patch does what we expect res = jsonpatch.apply_patch(src, patch) @@ -700,7 +715,7 @@ def test_minimal_patch(self): src = [{"foo": 1, "bar": 2}] dst = [{"foo": 2, "bar": 2}] - patch = jsonpatch.make_patch(src, dst) + patch = jsonpatch.make_patch(src, dst, generate_test_ops=self.generate_test_ops) exp = [ { @@ -710,9 +725,16 @@ def test_minimal_patch(self): } ] + if self.generate_test_ops: + exp.insert(0, {"path": "/0/foo", "value": 1, "op": "test"}) + self.assertEqual(patch.patch, exp) +class OptimizationWithTestOpsTests(OptimizationTests): + generate_test_ops = True + + class ListTests(unittest.TestCase): def test_fail_prone_list_1(self): @@ -1073,6 +1095,7 @@ def get_suite(): suite.addTest(unittest.makeSuite(InvalidInputTests)) suite.addTest(unittest.makeSuite(ConflictTests)) suite.addTest(unittest.makeSuite(OptimizationTests)) + suite.addTest(unittest.makeSuite(OptimizationWithTestOpsTests)) suite.addTest(unittest.makeSuite(JsonPointerTests)) suite.addTest(unittest.makeSuite(JsonPatchCreationTest)) suite.addTest(unittest.makeSuite(UtilityMethodTests)) From ee5040f59b7579ce0d68b98b6300e59a7d1d002b Mon Sep 17 00:00:00 2001 From: Robert Cochran Date: Fri, 22 Mar 2024 15:29:35 -0700 Subject: [PATCH 7/7] Remove pregenerated test op when remove is converted to move --- jsonpatch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jsonpatch.py b/jsonpatch.py index 840cbe0..850c51d 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -874,6 +874,8 @@ def _item_removed(self, path, key, item): 'path': op.location, }, pointer_cls=self.pointer_cls) new_index[2] = new_op + if self.generate_test_ops: + self.remove(test_index) else: self.remove(new_index)