use new (more verbose) syntax from spec draft 5
This commit is contained in:
63
jsonpatch.py
63
jsonpatch.py
@@ -84,12 +84,12 @@ def apply_patch(doc, patch, in_place=False):
|
|||||||
:rtype: dict
|
:rtype: dict
|
||||||
|
|
||||||
>>> doc = {'foo': 'bar'}
|
>>> 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
|
>>> doc is not other
|
||||||
True
|
True
|
||||||
>>> other
|
>>> other
|
||||||
{'foo': 'bar', 'baz': 'qux'}
|
{'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'}
|
{'foo': 'bar', 'baz': 'qux'}
|
||||||
>>> doc == other
|
>>> doc == other
|
||||||
True
|
True
|
||||||
@@ -125,23 +125,26 @@ class JsonPatch(object):
|
|||||||
"""A JSON Patch is a list of Patch Operations.
|
"""A JSON Patch is a list of Patch Operations.
|
||||||
|
|
||||||
>>> patch = JsonPatch([
|
>>> patch = JsonPatch([
|
||||||
... {'add': '/foo', 'value': 'bar'},
|
... {'op': 'add', 'path': '/foo', 'value': 'bar'},
|
||||||
... {'add': '/baz', 'value': [1, 2, 3]},
|
... {'op': 'add', 'path': '/baz', 'value': [1, 2, 3]},
|
||||||
... {'remove': '/baz/1'},
|
... {'op': 'remove', 'path': '/baz/1'},
|
||||||
... {'test': '/baz', 'value': [1, 3]},
|
... {'op': 'test', 'path': '/baz', 'value': [1, 3]},
|
||||||
... {'replace': '/baz/0', 'value': 42},
|
... {'op': 'replace', 'path': '/baz/0', 'value': 42},
|
||||||
... {'remove': '/baz/1'},
|
... {'op': 'remove', 'path': '/baz/1'},
|
||||||
... ])
|
... ])
|
||||||
>>> doc = {}
|
>>> doc = {}
|
||||||
>>> patch.apply(doc)
|
>>> result = patch.apply(doc)
|
||||||
{'foo': 'bar', 'baz': [42]}
|
>>> expected = {'foo': 'bar', 'baz': [42]}
|
||||||
|
>>> result == expected
|
||||||
|
True
|
||||||
|
|
||||||
JsonPatch object is iterable, so you could easily access to each patch
|
JsonPatch object is iterable, so you could easily access to each patch
|
||||||
statement in loop:
|
statement in loop:
|
||||||
|
|
||||||
>>> lpatch = list(patch)
|
>>> lpatch = list(patch)
|
||||||
>>> lpatch[0]
|
>>> expected = {'op': 'add', 'path': '/foo', 'value': 'bar'}
|
||||||
{'add': '/foo', 'value': 'bar'}
|
>>> lpatch[0] == expected
|
||||||
|
True
|
||||||
>>> lpatch == patch.patch
|
>>> lpatch == patch.patch
|
||||||
True
|
True
|
||||||
|
|
||||||
@@ -231,19 +234,19 @@ class JsonPatch(object):
|
|||||||
for operation in compare_list(path, value, other):
|
for operation in compare_list(path, value, other):
|
||||||
yield operation
|
yield operation
|
||||||
else:
|
else:
|
||||||
yield {'replace': '/'.join(path), 'value': other}
|
yield {'op': 'replace', 'path': '/'.join(path), 'value': other}
|
||||||
|
|
||||||
def compare_dict(path, src, dst):
|
def compare_dict(path, src, dst):
|
||||||
for key in src:
|
for key in src:
|
||||||
if key not in dst:
|
if key not in dst:
|
||||||
yield {'remove': '/'.join(path + [key])}
|
yield {'op': 'remove', 'path': '/'.join(path + [key])}
|
||||||
continue
|
continue
|
||||||
current = path + [key]
|
current = path + [key]
|
||||||
for operation in compare_values(current, src[key], dst[key]):
|
for operation in compare_values(current, src[key], dst[key]):
|
||||||
yield operation
|
yield operation
|
||||||
for key in dst:
|
for key in dst:
|
||||||
if key not in src:
|
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):
|
def compare_list(path, src, dst):
|
||||||
lsrc, ldst = len(src), len(dst)
|
lsrc, ldst = len(src), len(dst)
|
||||||
@@ -254,10 +257,10 @@ class JsonPatch(object):
|
|||||||
if lsrc < ldst:
|
if lsrc < ldst:
|
||||||
for idx in range(lsrc, ldst):
|
for idx in range(lsrc, ldst):
|
||||||
current = path + [str(idx)]
|
current = path + [str(idx)]
|
||||||
yield {'add': '/'.join(current), 'value': dst[idx]}
|
yield {'op': 'add', 'path': '/'.join(current), 'value': dst[idx]}
|
||||||
elif lsrc > ldst:
|
elif lsrc > ldst:
|
||||||
for idx in reversed(range(ldst, lsrc)):
|
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)))
|
return cls(list(compare_dict([''], src, dst)))
|
||||||
|
|
||||||
@@ -288,19 +291,24 @@ class JsonPatch(object):
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
def _get_operation(self, operation):
|
def _get_operation(self, operation):
|
||||||
for action, op_cls in self.operations.items():
|
if 'op' not in operation:
|
||||||
if action in operation:
|
raise JsonPatchException("Operation does not contain 'op' member")
|
||||||
location = operation[action]
|
raise Exception
|
||||||
return op_cls(location, operation)
|
|
||||||
|
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):
|
class PatchOperation(object):
|
||||||
"""A single operation inside a JSON Patch."""
|
"""A single operation inside a JSON Patch."""
|
||||||
|
|
||||||
def __init__(self, location, operation):
|
def __init__(self, operation):
|
||||||
self.location = location
|
self.location = operation['path']
|
||||||
self.operation = operation
|
self.operation = operation
|
||||||
|
|
||||||
def apply(self, obj):
|
def apply(self, obj):
|
||||||
@@ -407,8 +415,9 @@ class MoveOperation(PatchOperation):
|
|||||||
def apply(self, obj):
|
def apply(self, obj):
|
||||||
subobj, part = self.locate(obj, self.location)
|
subobj, part = self.locate(obj, self.location)
|
||||||
value = subobj[part]
|
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):
|
class TestOperation(PatchOperation):
|
||||||
@@ -434,4 +443,4 @@ class CopyOperation(PatchOperation):
|
|||||||
def apply(self, obj):
|
def apply(self, obj):
|
||||||
subobj, part = self.locate(obj, self.location)
|
subobj, part = self.locate(obj, self.location)
|
||||||
value = copy.deepcopy(subobj[part])
|
value = copy.deepcopy(subobj[part])
|
||||||
AddOperation(self.operation['to'], {'value': value}).apply(obj)
|
AddOperation({'op': 'add', 'path': self.operation['to'], 'value': value}).apply(obj)
|
||||||
|
|||||||
46
tests.py
46
tests.py
@@ -10,7 +10,7 @@ class ApplyPatchTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
def test_apply_patch_from_string(self):
|
def test_apply_patch_from_string(self):
|
||||||
obj = {'foo': 'bar'}
|
obj = {'foo': 'bar'}
|
||||||
patch = '[{"add": "/baz", "value": "qux"}]'
|
patch = '[{"op": "add", "path": "/baz", "value": "qux"}]'
|
||||||
res = jsonpatch.apply_patch(obj, patch)
|
res = jsonpatch.apply_patch(obj, patch)
|
||||||
self.assertTrue(obj is not res)
|
self.assertTrue(obj is not res)
|
||||||
self.assertTrue('baz' in res)
|
self.assertTrue('baz' in res)
|
||||||
@@ -18,71 +18,71 @@ class ApplyPatchTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
def test_apply_patch_to_copy(self):
|
def test_apply_patch_to_copy(self):
|
||||||
obj = {'foo': 'bar'}
|
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)
|
self.assertTrue(obj is not res)
|
||||||
|
|
||||||
def test_apply_patch_to_same_instance(self):
|
def test_apply_patch_to_same_instance(self):
|
||||||
obj = {'foo': 'bar'}
|
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)
|
in_place=True)
|
||||||
self.assertTrue(obj is res)
|
self.assertTrue(obj is res)
|
||||||
|
|
||||||
def test_add_object_key(self):
|
def test_add_object_key(self):
|
||||||
obj = {'foo': 'bar'}
|
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.assertTrue('baz' in res)
|
||||||
self.assertEqual(res['baz'], 'qux')
|
self.assertEqual(res['baz'], 'qux')
|
||||||
|
|
||||||
def test_add_array_item(self):
|
def test_add_array_item(self):
|
||||||
obj = {'foo': ['bar', 'baz']}
|
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'])
|
self.assertEqual(res['foo'], ['bar', 'qux', 'baz'])
|
||||||
|
|
||||||
def test_remove_object_key(self):
|
def test_remove_object_key(self):
|
||||||
obj = {'foo': 'bar', 'baz': 'qux'}
|
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)
|
self.assertTrue('baz' not in res)
|
||||||
|
|
||||||
def test_remove_array_item(self):
|
def test_remove_array_item(self):
|
||||||
obj = {'foo': ['bar', 'qux', 'baz']}
|
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'])
|
self.assertEqual(res['foo'], ['bar', 'baz'])
|
||||||
|
|
||||||
def test_replace_object_key(self):
|
def test_replace_object_key(self):
|
||||||
obj = {'foo': 'bar', 'baz': 'qux'}
|
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')
|
self.assertTrue(res['baz'], 'boo')
|
||||||
|
|
||||||
def test_replace_array_item(self):
|
def test_replace_array_item(self):
|
||||||
obj = {'foo': ['bar', 'qux', 'baz']}
|
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'}])
|
'value': 'boo'}])
|
||||||
self.assertEqual(res['foo'], ['bar', 'boo', 'baz'])
|
self.assertEqual(res['foo'], ['bar', 'boo', 'baz'])
|
||||||
|
|
||||||
def test_move_object_key(self):
|
def test_move_object_key(self):
|
||||||
obj = {'foo': {'bar': 'baz', 'waldo': 'fred'},
|
obj = {'foo': {'bar': 'baz', 'waldo': 'fred'},
|
||||||
'qux': {'corge': 'grault'}}
|
'qux': {'corge': 'grault'}}
|
||||||
res = jsonpatch.apply_patch(obj, [{'move': '/foo/waldo',
|
res = jsonpatch.apply_patch(obj, [{'op': 'move', 'path': '/foo/waldo',
|
||||||
'to': '/qux/thud'}])
|
'to': '/qux/thud'}])
|
||||||
self.assertEqual(res, {'qux': {'thud': 'fred', 'corge': 'grault'},
|
self.assertEqual(res, {'qux': {'thud': 'fred', 'corge': 'grault'},
|
||||||
'foo': {'bar': 'baz'}})
|
'foo': {'bar': 'baz'}})
|
||||||
|
|
||||||
def test_move_array_item(self):
|
def test_move_array_item(self):
|
||||||
obj = {'foo': ['all', 'grass', 'cows', 'eat']}
|
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']})
|
self.assertEqual(res, {'foo': ['all', 'cows', 'eat', 'grass']})
|
||||||
|
|
||||||
def test_copy_object_key(self):
|
def test_copy_object_key(self):
|
||||||
obj = {'foo': {'bar': 'baz', 'waldo': 'fred'},
|
obj = {'foo': {'bar': 'baz', 'waldo': 'fred'},
|
||||||
'qux': {'corge': 'grault'}}
|
'qux': {'corge': 'grault'}}
|
||||||
res = jsonpatch.apply_patch(obj, [{'copy': '/foo/waldo',
|
res = jsonpatch.apply_patch(obj, [{'op': 'copy', 'path': '/foo/waldo',
|
||||||
'to': '/qux/thud'}])
|
'to': '/qux/thud'}])
|
||||||
self.assertEqual(res, {'qux': {'thud': 'fred', 'corge': 'grault'},
|
self.assertEqual(res, {'qux': {'thud': 'fred', 'corge': 'grault'},
|
||||||
'foo': {'bar': 'baz', 'waldo': 'fred'}})
|
'foo': {'bar': 'baz', 'waldo': 'fred'}})
|
||||||
|
|
||||||
def test_copy_array_item(self):
|
def test_copy_array_item(self):
|
||||||
obj = {'foo': ['all', 'grass', 'cows', 'eat']}
|
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']})
|
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 """
|
""" test if mutable objects (dicts and lists) are copied by value """
|
||||||
obj = {'foo': [{'bar': 42}, {'baz': 3.14}], 'boo': []}
|
obj = {'foo': [{'bar': 42}, {'baz': 3.14}], 'boo': []}
|
||||||
# copy object somewhere
|
# 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}]})
|
self.assertEqual(res, {'foo': [{'bar': 42}, {'baz': 3.14}], 'boo': [{'bar': 42}]})
|
||||||
# modify original object
|
# 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
|
# check if that didn't modify the copied object
|
||||||
self.assertEqual(res['boo'], [{'bar': 42}])
|
self.assertEqual(res['boo'], [{'bar': 42}])
|
||||||
|
|
||||||
|
|
||||||
def test_test_success(self):
|
def test_test_success(self):
|
||||||
obj = {'baz': 'qux', 'foo': ['a', 2, 'c']}
|
obj = {'baz': 'qux', 'foo': ['a', 2, 'c']}
|
||||||
jsonpatch.apply_patch(obj, [{'test': '/baz', 'value': 'qux'},
|
jsonpatch.apply_patch(obj, [{'op': 'test', 'path': '/baz', 'value': 'qux'},
|
||||||
{'test': '/foo/1', 'value': 2}])
|
{'op': 'test', 'path': '/foo/1', 'value': 2}])
|
||||||
|
|
||||||
def test_test_error(self):
|
def test_test_error(self):
|
||||||
obj = {'bar': 'qux'}
|
obj = {'bar': 'qux'}
|
||||||
self.assertRaises(jsonpatch.JsonPatchTestFailed,
|
self.assertRaises(jsonpatch.JsonPatchTestFailed,
|
||||||
jsonpatch.apply_patch,
|
jsonpatch.apply_patch,
|
||||||
obj, [{'test': '/bar', 'value': 'bar'}])
|
obj, [{'op': 'test', 'path': '/bar', 'value': 'bar'}])
|
||||||
|
|
||||||
|
|
||||||
def test_test_not_existing(self):
|
def test_test_not_existing(self):
|
||||||
obj = {'bar': 'qux'}
|
obj = {'bar': 'qux'}
|
||||||
self.assertRaises(jsonpatch.JsonPatchTestFailed,
|
self.assertRaises(jsonpatch.JsonPatchTestFailed,
|
||||||
jsonpatch.apply_patch,
|
jsonpatch.apply_patch,
|
||||||
obj, [{'test': '/baz', 'value': 'bar'}])
|
obj, [{'op': 'test', 'path': '/baz', 'value': 'bar'}])
|
||||||
|
|
||||||
|
|
||||||
def test_test_noval_existing(self):
|
def test_test_noval_existing(self):
|
||||||
obj = {'bar': 'qux'}
|
obj = {'bar': 'qux'}
|
||||||
jsonpatch.apply_patch(obj, [{'test': '/bar'}])
|
jsonpatch.apply_patch(obj, [{'op': 'test', 'path': '/bar'}])
|
||||||
|
|
||||||
|
|
||||||
def test_test_noval_not_existing(self):
|
def test_test_noval_not_existing(self):
|
||||||
obj = {'bar': 'qux'}
|
obj = {'bar': 'qux'}
|
||||||
self.assertRaises(jsonpatch.JsonPatchTestFailed,
|
self.assertRaises(jsonpatch.JsonPatchTestFailed,
|
||||||
jsonpatch.apply_patch,
|
jsonpatch.apply_patch,
|
||||||
obj, [{'test': '/baz'}])
|
obj, [{'op': 'test', 'path': '/baz'}])
|
||||||
|
|
||||||
|
|
||||||
def test_test_noval_not_existing_nested(self):
|
def test_test_noval_not_existing_nested(self):
|
||||||
obj = {'bar': {'qux': 2}}
|
obj = {'bar': {'qux': 2}}
|
||||||
self.assertRaises(jsonpatch.JsonPatchTestFailed,
|
self.assertRaises(jsonpatch.JsonPatchTestFailed,
|
||||||
jsonpatch.apply_patch,
|
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):
|
def test_add_nested(self):
|
||||||
# see http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-03#appendix-A.10
|
# see http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-03#appendix-A.10
|
||||||
src = {"foo": "bar"}
|
src = {"foo": "bar"}
|
||||||
patch_obj = [ { "add": "/child", "value": { "grandchild": { } } } ]
|
patch_obj = [ { "op": "add", "path": "/child", "value": { "grandchild": { } } } ]
|
||||||
res = jsonpatch.apply_patch(src, patch_obj)
|
res = jsonpatch.apply_patch(src, patch_obj)
|
||||||
expected = { "foo": "bar",
|
expected = { "foo": "bar",
|
||||||
"child": { "grandchild": { } }
|
"child": { "grandchild": { } }
|
||||||
|
|||||||
Reference in New Issue
Block a user