From d935044158c3bdcd2ca1014166d7ca323cd57c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Fri, 28 Sep 2012 18:46:55 +0200 Subject: [PATCH] use new (more verbose) syntax from spec draft 5 --- jsonpatch.py | 63 ++++++++++++++++++++++++++++++---------------------- tests.py | 46 +++++++++++++++++++------------------- 2 files changed, 59 insertions(+), 50 deletions(-) diff --git a/jsonpatch.py b/jsonpatch.py index 6f3b776..6aa4878 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -84,12 +84,12 @@ def apply_patch(doc, patch, in_place=False): :rtype: dict >>> doc = {'foo': 'bar'} - >>> other = apply_patch(doc, [{'add': '/baz', 'value': 'qux'}]) + >>> other = apply_patch(doc, [{'op': 'add', 'path': '/baz', 'value': 'qux'}]) >>> doc is not other True >>> other {'foo': 'bar', 'baz': 'qux'} - >>> apply_patch(doc, [{'add': '/baz', 'value': 'qux'}], in_place=True) + >>> apply_patch(doc, [{'op': 'add', 'path': '/baz', 'value': 'qux'}], in_place=True) {'foo': 'bar', 'baz': 'qux'} >>> doc == other True @@ -125,23 +125,26 @@ class JsonPatch(object): """A JSON Patch is a list of Patch Operations. >>> patch = JsonPatch([ - ... {'add': '/foo', 'value': 'bar'}, - ... {'add': '/baz', 'value': [1, 2, 3]}, - ... {'remove': '/baz/1'}, - ... {'test': '/baz', 'value': [1, 3]}, - ... {'replace': '/baz/0', 'value': 42}, - ... {'remove': '/baz/1'}, + ... {'op': 'add', 'path': '/foo', 'value': 'bar'}, + ... {'op': 'add', 'path': '/baz', 'value': [1, 2, 3]}, + ... {'op': 'remove', 'path': '/baz/1'}, + ... {'op': 'test', 'path': '/baz', 'value': [1, 3]}, + ... {'op': 'replace', 'path': '/baz/0', 'value': 42}, + ... {'op': 'remove', 'path': '/baz/1'}, ... ]) >>> doc = {} - >>> patch.apply(doc) - {'foo': 'bar', 'baz': [42]} + >>> result = patch.apply(doc) + >>> expected = {'foo': 'bar', 'baz': [42]} + >>> result == expected + True JsonPatch object is iterable, so you could easily access to each patch statement in loop: >>> lpatch = list(patch) - >>> lpatch[0] - {'add': '/foo', 'value': 'bar'} + >>> expected = {'op': 'add', 'path': '/foo', 'value': 'bar'} + >>> lpatch[0] == expected + True >>> lpatch == patch.patch True @@ -231,19 +234,19 @@ class JsonPatch(object): for operation in compare_list(path, value, other): yield operation else: - yield {'replace': '/'.join(path), 'value': other} + yield {'op': 'replace', 'path': '/'.join(path), 'value': other} def compare_dict(path, src, dst): for key in src: if key not in dst: - yield {'remove': '/'.join(path + [key])} + yield {'op': 'remove', 'path': '/'.join(path + [key])} continue current = path + [key] for operation in compare_values(current, src[key], dst[key]): yield operation for key in dst: if key not in src: - yield {'add': '/'.join(path + [key]), 'value': dst[key]} + yield {'op': 'add', 'path': '/'.join(path + [key]), 'value': dst[key]} def compare_list(path, src, dst): lsrc, ldst = len(src), len(dst) @@ -254,10 +257,10 @@ class JsonPatch(object): if lsrc < ldst: for idx in range(lsrc, ldst): current = path + [str(idx)] - yield {'add': '/'.join(current), 'value': dst[idx]} + yield {'op': 'add', 'path': '/'.join(current), 'value': dst[idx]} elif lsrc > ldst: for idx in reversed(range(ldst, lsrc)): - yield {'remove': '/'.join(path + [str(idx)])} + yield {'op': 'remove', 'path': '/'.join(path + [str(idx)])} return cls(list(compare_dict([''], src, dst))) @@ -288,19 +291,24 @@ class JsonPatch(object): return obj def _get_operation(self, operation): - for action, op_cls in self.operations.items(): - if action in operation: - location = operation[action] - return op_cls(location, operation) + if 'op' not in operation: + raise JsonPatchException("Operation does not contain 'op' member") + raise Exception + + op = operation['op'] + if op not in self.operations: + raise JsonPatchException("Unknown operation '%s'" % op) + + cls = self.operations[op] + return cls(operation) - raise JsonPatchException("invalid operation '%s'" % operation) class PatchOperation(object): """A single operation inside a JSON Patch.""" - def __init__(self, location, operation): - self.location = location + def __init__(self, operation): + self.location = operation['path'] self.operation = operation def apply(self, obj): @@ -407,8 +415,9 @@ class MoveOperation(PatchOperation): def apply(self, obj): subobj, part = self.locate(obj, self.location) value = subobj[part] - RemoveOperation(self.location, self.operation).apply(obj) - AddOperation(self.operation['to'], {'value': value}).apply(obj) + + RemoveOperation({'op': 'remove', 'path': self.location}).apply(obj) + AddOperation({'op': 'add', 'path': self.operation['to'], 'value': value}).apply(obj) class TestOperation(PatchOperation): @@ -434,4 +443,4 @@ class CopyOperation(PatchOperation): def apply(self, obj): subobj, part = self.locate(obj, self.location) value = copy.deepcopy(subobj[part]) - AddOperation(self.operation['to'], {'value': value}).apply(obj) + AddOperation({'op': 'add', 'path': self.operation['to'], 'value': value}).apply(obj) diff --git a/tests.py b/tests.py index 340c10a..72fa584 100755 --- a/tests.py +++ b/tests.py @@ -10,7 +10,7 @@ class ApplyPatchTestCase(unittest.TestCase): def test_apply_patch_from_string(self): obj = {'foo': 'bar'} - patch = '[{"add": "/baz", "value": "qux"}]' + patch = '[{"op": "add", "path": "/baz", "value": "qux"}]' res = jsonpatch.apply_patch(obj, patch) self.assertTrue(obj is not res) self.assertTrue('baz' in res) @@ -18,71 +18,71 @@ class ApplyPatchTestCase(unittest.TestCase): def test_apply_patch_to_copy(self): obj = {'foo': 'bar'} - res = jsonpatch.apply_patch(obj, [{'add': '/baz', 'value': 'qux'}]) + res = jsonpatch.apply_patch(obj, [{'op': 'add', 'path': '/baz', 'value': 'qux'}]) self.assertTrue(obj is not res) def test_apply_patch_to_same_instance(self): obj = {'foo': 'bar'} - res = jsonpatch.apply_patch(obj, [{'add': '/baz', 'value': 'qux'}], + res = jsonpatch.apply_patch(obj, [{'op': 'add', 'path': '/baz', 'value': 'qux'}], in_place=True) self.assertTrue(obj is res) def test_add_object_key(self): obj = {'foo': 'bar'} - res = jsonpatch.apply_patch(obj, [{'add': '/baz', 'value': 'qux'}]) + res = jsonpatch.apply_patch(obj, [{'op': 'add', 'path': '/baz', 'value': 'qux'}]) self.assertTrue('baz' in res) self.assertEqual(res['baz'], 'qux') def test_add_array_item(self): obj = {'foo': ['bar', 'baz']} - res = jsonpatch.apply_patch(obj, [{'add': '/foo/1', 'value': 'qux'}]) + res = jsonpatch.apply_patch(obj, [{'op': 'add', 'path': '/foo/1', 'value': 'qux'}]) self.assertEqual(res['foo'], ['bar', 'qux', 'baz']) def test_remove_object_key(self): obj = {'foo': 'bar', 'baz': 'qux'} - res = jsonpatch.apply_patch(obj, [{'remove': '/baz'}]) + res = jsonpatch.apply_patch(obj, [{'op': 'remove', 'path': '/baz'}]) self.assertTrue('baz' not in res) def test_remove_array_item(self): obj = {'foo': ['bar', 'qux', 'baz']} - res = jsonpatch.apply_patch(obj, [{'remove': '/foo/1'}]) + res = jsonpatch.apply_patch(obj, [{'op': 'remove', 'path': '/foo/1'}]) self.assertEqual(res['foo'], ['bar', 'baz']) def test_replace_object_key(self): obj = {'foo': 'bar', 'baz': 'qux'} - res = jsonpatch.apply_patch(obj, [{'replace': '/baz', 'value': 'boo'}]) + res = jsonpatch.apply_patch(obj, [{'op': 'replace', 'path': '/baz', 'value': 'boo'}]) self.assertTrue(res['baz'], 'boo') def test_replace_array_item(self): obj = {'foo': ['bar', 'qux', 'baz']} - res = jsonpatch.apply_patch(obj, [{'replace': '/foo/1', + res = jsonpatch.apply_patch(obj, [{'op': 'replace', 'path': '/foo/1', 'value': 'boo'}]) self.assertEqual(res['foo'], ['bar', 'boo', 'baz']) def test_move_object_key(self): obj = {'foo': {'bar': 'baz', 'waldo': 'fred'}, 'qux': {'corge': 'grault'}} - res = jsonpatch.apply_patch(obj, [{'move': '/foo/waldo', + res = jsonpatch.apply_patch(obj, [{'op': 'move', 'path': '/foo/waldo', 'to': '/qux/thud'}]) self.assertEqual(res, {'qux': {'thud': 'fred', 'corge': 'grault'}, 'foo': {'bar': 'baz'}}) def test_move_array_item(self): obj = {'foo': ['all', 'grass', 'cows', 'eat']} - res = jsonpatch.apply_patch(obj, [{'move': '/foo/1', 'to': '/foo/3'}]) + res = jsonpatch.apply_patch(obj, [{'op': 'move', 'path': '/foo/1', 'to': '/foo/3'}]) self.assertEqual(res, {'foo': ['all', 'cows', 'eat', 'grass']}) def test_copy_object_key(self): obj = {'foo': {'bar': 'baz', 'waldo': 'fred'}, 'qux': {'corge': 'grault'}} - res = jsonpatch.apply_patch(obj, [{'copy': '/foo/waldo', + res = jsonpatch.apply_patch(obj, [{'op': 'copy', 'path': '/foo/waldo', 'to': '/qux/thud'}]) self.assertEqual(res, {'qux': {'thud': 'fred', 'corge': 'grault'}, 'foo': {'bar': 'baz', 'waldo': 'fred'}}) def test_copy_array_item(self): obj = {'foo': ['all', 'grass', 'cows', 'eat']} - res = jsonpatch.apply_patch(obj, [{'copy': '/foo/1', 'to': '/foo/3'}]) + res = jsonpatch.apply_patch(obj, [{'op': 'copy', 'path': '/foo/1', 'to': '/foo/3'}]) self.assertEqual(res, {'foo': ['all', 'grass', 'cows', 'grass', 'eat']}) @@ -90,50 +90,50 @@ class ApplyPatchTestCase(unittest.TestCase): """ test if mutable objects (dicts and lists) are copied by value """ obj = {'foo': [{'bar': 42}, {'baz': 3.14}], 'boo': []} # copy object somewhere - res = jsonpatch.apply_patch(obj, [{'copy': '/foo/0', 'to': '/boo/0' }]) + res = jsonpatch.apply_patch(obj, [{'op': 'copy', 'path': '/foo/0', 'to': '/boo/0' }]) self.assertEqual(res, {'foo': [{'bar': 42}, {'baz': 3.14}], 'boo': [{'bar': 42}]}) # modify original object - res = jsonpatch.apply_patch(res, [{'add': '/foo/0/zoo', 'value': 255}]) + res = jsonpatch.apply_patch(res, [{'op': 'add', 'path': '/foo/0/zoo', 'value': 255}]) # check if that didn't modify the copied object self.assertEqual(res['boo'], [{'bar': 42}]) def test_test_success(self): obj = {'baz': 'qux', 'foo': ['a', 2, 'c']} - jsonpatch.apply_patch(obj, [{'test': '/baz', 'value': 'qux'}, - {'test': '/foo/1', 'value': 2}]) + jsonpatch.apply_patch(obj, [{'op': 'test', 'path': '/baz', 'value': 'qux'}, + {'op': 'test', 'path': '/foo/1', 'value': 2}]) def test_test_error(self): obj = {'bar': 'qux'} self.assertRaises(jsonpatch.JsonPatchTestFailed, jsonpatch.apply_patch, - obj, [{'test': '/bar', 'value': 'bar'}]) + obj, [{'op': 'test', 'path': '/bar', 'value': 'bar'}]) def test_test_not_existing(self): obj = {'bar': 'qux'} self.assertRaises(jsonpatch.JsonPatchTestFailed, jsonpatch.apply_patch, - obj, [{'test': '/baz', 'value': 'bar'}]) + obj, [{'op': 'test', 'path': '/baz', 'value': 'bar'}]) def test_test_noval_existing(self): obj = {'bar': 'qux'} - jsonpatch.apply_patch(obj, [{'test': '/bar'}]) + jsonpatch.apply_patch(obj, [{'op': 'test', 'path': '/bar'}]) def test_test_noval_not_existing(self): obj = {'bar': 'qux'} self.assertRaises(jsonpatch.JsonPatchTestFailed, jsonpatch.apply_patch, - obj, [{'test': '/baz'}]) + obj, [{'op': 'test', 'path': '/baz'}]) def test_test_noval_not_existing_nested(self): obj = {'bar': {'qux': 2}} self.assertRaises(jsonpatch.JsonPatchTestFailed, jsonpatch.apply_patch, - obj, [{'test': '/baz/qx'}]) + obj, [{'op': 'test', 'path': '/baz/qx'}]) @@ -190,7 +190,7 @@ class MakePatchTestCase(unittest.TestCase): def test_add_nested(self): # see http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-03#appendix-A.10 src = {"foo": "bar"} - patch_obj = [ { "add": "/child", "value": { "grandchild": { } } } ] + patch_obj = [ { "op": "add", "path": "/child", "value": { "grandchild": { } } } ] res = jsonpatch.apply_patch(src, patch_obj) expected = { "foo": "bar", "child": { "grandchild": { } }