summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Kögl <stefan@skoegl.net>2020-11-23 20:40:14 +0100
committerGitHub <noreply@github.com>2020-11-23 20:40:14 +0100
commit3a956350391f644e61790df17c82f578c8020170 (patch)
tree21d584d7d59b6cd4a917417b1f5a022e2d0cfe1a
parent24b5e86dd66824dce232ea07e95d39a67b1dd735 (diff)
parenteca4f8ab337b8ca8e9db5d0d0d81f536d77fbd7d (diff)
downloadpython-json-patch-3a956350391f644e61790df17c82f578c8020170.tar.gz
Merge pull request #114 from tzoiker/feature/custom-pointer
Allow custom JSON pointer class
-rw-r--r--jsonpatch.py81
-rwxr-xr-xtests.py130
2 files changed, 180 insertions, 31 deletions
diff --git a/jsonpatch.py b/jsonpatch.py
index dc54efc..5522d50 100644
--- a/jsonpatch.py
+++ b/jsonpatch.py
@@ -106,7 +106,7 @@ def multidict(ordered_pairs):
_jsonloads = functools.partial(json.loads, object_pairs_hook=multidict)
-def apply_patch(doc, patch, in_place=False):
+def apply_patch(doc, patch, in_place=False, pointer_cls=JsonPointer):
"""Apply list of patches to specified json document.
:param doc: Document object.
@@ -119,6 +119,9 @@ def apply_patch(doc, patch, in_place=False):
By default patch will be applied to document copy.
:type in_place: bool
+ :param pointer_cls: JSON pointer class to use.
+ :type pointer_cls: Type[JsonPointer]
+
:return: Patched document object.
:rtype: dict
@@ -137,13 +140,13 @@ def apply_patch(doc, patch, in_place=False):
"""
if isinstance(patch, basestring):
- patch = JsonPatch.from_string(patch)
+ patch = JsonPatch.from_string(patch, pointer_cls=pointer_cls)
else:
- patch = JsonPatch(patch)
+ patch = JsonPatch(patch, pointer_cls=pointer_cls)
return patch.apply(doc, in_place)
-def make_patch(src, dst):
+def make_patch(src, dst, pointer_cls=JsonPointer):
"""Generates patch by comparing two document objects. Actually is
a proxy to :meth:`JsonPatch.from_diff` method.
@@ -153,6 +156,9 @@ def make_patch(src, dst):
:param dst: Data source document object.
:type dst: dict
+ :param pointer_cls: JSON pointer class to use.
+ :type pointer_cls: Type[JsonPointer]
+
>>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]}
>>> dst = {'baz': 'qux', 'numbers': [1, 4, 7]}
>>> patch = make_patch(src, dst)
@@ -161,7 +167,7 @@ def make_patch(src, dst):
True
"""
- return JsonPatch.from_diff(src, dst)
+ return JsonPatch.from_diff(src, dst, pointer_cls=pointer_cls)
class JsonPatch(object):
@@ -213,8 +219,9 @@ class JsonPatch(object):
... patch.apply(old) #doctest: +ELLIPSIS
{...}
"""
- def __init__(self, patch):
+ def __init__(self, patch, pointer_cls=JsonPointer):
self.patch = patch
+ self.pointer_cls = pointer_cls
self.operations = {
'remove': RemoveOperation,
@@ -256,23 +263,30 @@ class JsonPatch(object):
return not(self == other)
@classmethod
- def from_string(cls, patch_str, loads=None):
+ def from_string(cls, patch_str, loads=None, pointer_cls=JsonPointer):
"""Creates JsonPatch instance from string source.
:param patch_str: JSON patch as raw string.
:type patch_str: str
+
:param loads: A function of one argument that loads a serialized
JSON string.
:type loads: function
+ :param pointer_cls: JSON pointer class to use.
+ :type pointer_cls: Type[JsonPointer]
+
:return: :class:`JsonPatch` instance.
"""
json_loader = loads or cls.json_loader
patch = json_loader(patch_str)
- return cls(patch)
+ return cls(patch, pointer_cls=pointer_cls)
@classmethod
- def from_diff(cls, src, dst, optimization=True, dumps=None):
+ def from_diff(
+ cls, src, dst, optimization=True, dumps=None,
+ pointer_cls=JsonPointer,
+ ):
"""Creates JsonPatch instance based on comparison of two document
objects. Json patch would be created for `src` argument against `dst`
one.
@@ -287,6 +301,9 @@ class JsonPatch(object):
JSON string.
:type dumps: function
+ :param pointer_cls: JSON pointer class to use.
+ :type pointer_cls: Type[JsonPointer]
+
:return: :class:`JsonPatch` instance.
>>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]}
@@ -297,10 +314,10 @@ class JsonPatch(object):
True
"""
json_dumper = dumps or cls.json_dumper
- builder = DiffBuilder(json_dumper)
+ builder = DiffBuilder(json_dumper, pointer_cls=pointer_cls)
builder._compare_values('', None, src, dst)
ops = list(builder.execute())
- return cls(ops)
+ return cls(ops, pointer_cls=pointer_cls)
def to_string(self, dumps=None):
"""Returns patch set as JSON string."""
@@ -345,24 +362,25 @@ class JsonPatch(object):
raise InvalidJsonPatch("Unknown operation {0!r}".format(op))
cls = self.operations[op]
- return cls(operation)
+ return cls(operation, pointer_cls=self.pointer_cls)
class PatchOperation(object):
"""A single operation inside a JSON Patch."""
- def __init__(self, operation):
+ def __init__(self, operation, pointer_cls=JsonPointer):
+ self.pointer_cls = pointer_cls
if not operation.__contains__('path'):
raise InvalidJsonPatch("Operation must have a 'path' member")
- if isinstance(operation['path'], JsonPointer):
+ if isinstance(operation['path'], self.pointer_cls):
self.location = operation['path'].path
self.pointer = operation['path']
else:
self.location = operation['path']
try:
- self.pointer = JsonPointer(self.location)
+ self.pointer = self.pointer_cls(self.location)
except TypeError as ex:
raise InvalidJsonPatch("Invalid 'path'")
@@ -530,10 +548,10 @@ class MoveOperation(PatchOperation):
def apply(self, obj):
try:
- if isinstance(self.operation['from'], JsonPointer):
+ if isinstance(self.operation['from'], self.pointer_cls):
from_ptr = self.operation['from']
else:
- from_ptr = JsonPointer(self.operation['from'])
+ from_ptr = self.pointer_cls(self.operation['from'])
except KeyError as ex:
raise InvalidJsonPatch(
"The operation does not contain a 'from' member")
@@ -555,24 +573,24 @@ class MoveOperation(PatchOperation):
obj = RemoveOperation({
'op': 'remove',
'path': self.operation['from']
- }).apply(obj)
+ }, pointer_cls=self.pointer_cls).apply(obj)
obj = AddOperation({
'op': 'add',
'path': self.location,
'value': value
- }).apply(obj)
+ }, pointer_cls=self.pointer_cls).apply(obj)
return obj
@property
def from_path(self):
- from_ptr = JsonPointer(self.operation['from'])
+ from_ptr = self.pointer_cls(self.operation['from'])
return '/'.join(from_ptr.parts[:-1])
@property
def from_key(self):
- from_ptr = JsonPointer(self.operation['from'])
+ from_ptr = self.pointer_cls(self.operation['from'])
try:
return int(from_ptr.parts[-1])
except TypeError:
@@ -580,7 +598,7 @@ class MoveOperation(PatchOperation):
@from_key.setter
def from_key(self, value):
- from_ptr = JsonPointer(self.operation['from'])
+ from_ptr = self.pointer_cls(self.operation['from'])
from_ptr.parts[-1] = str(value)
self.operation['from'] = from_ptr.path
@@ -643,7 +661,7 @@ class CopyOperation(PatchOperation):
def apply(self, obj):
try:
- from_ptr = JsonPointer(self.operation['from'])
+ from_ptr = self.pointer_cls(self.operation['from'])
except KeyError as ex:
raise InvalidJsonPatch(
"The operation does not contain a 'from' member")
@@ -658,15 +676,16 @@ class CopyOperation(PatchOperation):
'op': 'add',
'path': self.location,
'value': value
- }).apply(obj)
+ }, pointer_cls=self.pointer_cls).apply(obj)
return obj
class DiffBuilder(object):
- def __init__(self, dumps=json.dumps):
+ def __init__(self, dumps=json.dumps, pointer_cls=JsonPointer):
self.dumps = dumps
+ self.pointer_cls = pointer_cls
self.index_storage = [{}, {}]
self.index_storage2 = [[], []]
self.__root = root = []
@@ -735,7 +754,7 @@ class DiffBuilder(object):
'op': 'replace',
'path': op_second.location,
'value': op_second.operation['value'],
- }).operation
+ }, pointer_cls=self.pointer_cls).operation
curr = curr[1][1]
continue
@@ -756,14 +775,14 @@ class DiffBuilder(object):
'op': 'move',
'from': op.location,
'path': _path_join(path, key),
- })
+ }, pointer_cls=self.pointer_cls)
self.insert(new_op)
else:
new_op = AddOperation({
'op': 'add',
'path': _path_join(path, key),
'value': item,
- })
+ }, pointer_cls=self.pointer_cls)
new_index = self.insert(new_op)
self.store_index(item, new_index, _ST_ADD)
@@ -771,7 +790,7 @@ class DiffBuilder(object):
new_op = RemoveOperation({
'op': 'remove',
'path': _path_join(path, key),
- })
+ }, pointer_cls=self.pointer_cls)
index = self.take_index(item, _ST_ADD)
new_index = self.insert(new_op)
if index is not None:
@@ -786,7 +805,7 @@ class DiffBuilder(object):
'op': 'move',
'from': new_op.location,
'path': op.location,
- })
+ }, pointer_cls=self.pointer_cls)
new_index[2] = new_op
else:
@@ -800,7 +819,7 @@ class DiffBuilder(object):
'op': 'replace',
'path': _path_join(path, key),
'value': item,
- }))
+ }, pointer_cls=self.pointer_cls))
def _compare_dicts(self, path, src, dst):
src_keys = set(src.keys())
diff --git a/tests.py b/tests.py
index b69f6e1..b5b7b9a 100755
--- a/tests.py
+++ b/tests.py
@@ -809,6 +809,135 @@ class UtilityMethodTests(unittest.TestCase):
jsonpatch.CopyOperation({'path': '/target', 'from': '/source'}).apply({})
+class CustomJsonPointer(jsonpointer.JsonPointer):
+ pass
+
+
+class PrefixJsonPointer(jsonpointer.JsonPointer):
+ def __init__(self, pointer):
+ super(PrefixJsonPointer, self).__init__('/foo/bar' + pointer)
+
+
+class CustomJsonPointerTests(unittest.TestCase):
+
+ def test_json_patch_from_string(self):
+ patch = '[{"op": "add", "path": "/baz", "value": "qux"}]'
+ res = jsonpatch.JsonPatch.from_string(
+ patch, pointer_cls=CustomJsonPointer,
+ )
+ self.assertEqual(res.pointer_cls, CustomJsonPointer)
+
+ def test_json_patch_from_object(self):
+ patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}]
+ res = jsonpatch.JsonPatch(
+ patch, pointer_cls=CustomJsonPointer,
+ )
+ self.assertEqual(res.pointer_cls, CustomJsonPointer)
+
+ def test_json_patch_from_diff(self):
+ old = {'foo': 'bar'}
+ new = {'foo': 'baz'}
+ res = jsonpatch.JsonPatch.from_diff(
+ old, new, pointer_cls=CustomJsonPointer,
+ )
+ self.assertEqual(res.pointer_cls, CustomJsonPointer)
+
+ def test_apply_patch_from_string(self):
+ obj = {'foo': 'bar'}
+ patch = '[{"op": "add", "path": "/baz", "value": "qux"}]'
+ res = jsonpatch.apply_patch(
+ obj, patch,
+ pointer_cls=CustomJsonPointer,
+ )
+ self.assertTrue(obj is not res)
+ self.assertTrue('baz' in res)
+ self.assertEqual(res['baz'], 'qux')
+
+ def test_apply_patch_from_object(self):
+ obj = {'foo': 'bar'}
+ res = jsonpatch.apply_patch(
+ obj, [{'op': 'add', 'path': '/baz', 'value': 'qux'}],
+ pointer_cls=CustomJsonPointer,
+ )
+ self.assertTrue(obj is not res)
+
+ def test_make_patch(self):
+ src = {'foo': 'bar', 'boo': 'qux'}
+ dst = {'baz': 'qux', 'foo': 'boo'}
+ patch = jsonpatch.make_patch(
+ src, dst, pointer_cls=CustomJsonPointer,
+ )
+ res = patch.apply(src)
+ self.assertTrue(src is not res)
+ self.assertEqual(patch.pointer_cls, CustomJsonPointer)
+ self.assertTrue(patch._ops)
+ for op in patch._ops:
+ self.assertIsInstance(op.pointer, CustomJsonPointer)
+ self.assertEqual(op.pointer_cls, CustomJsonPointer)
+
+ def test_operations(self):
+ operations =[
+ (
+ jsonpatch.AddOperation, {
+ 'op': 'add', 'path': '/foo', 'value': [1, 2, 3]
+ }
+ ),
+ (
+ jsonpatch.MoveOperation, {
+ 'op': 'move', 'path': '/baz', 'from': '/foo'
+ },
+ ),
+ (
+ jsonpatch.RemoveOperation, {
+ 'op': 'remove', 'path': '/baz/1'
+ },
+ ),
+ (
+ jsonpatch.TestOperation, {
+ 'op': 'test', 'path': '/baz', 'value': [1, 3]
+ },
+ ),
+ (
+ jsonpatch.ReplaceOperation, {
+ 'op': 'replace', 'path': '/baz/0', 'value': 42
+ },
+ ),
+ (
+ jsonpatch.RemoveOperation, {
+ 'op': 'remove', 'path': '/baz/1'
+ },
+ )
+ ]
+ for cls, patch in operations:
+ operation = cls(patch, pointer_cls=CustomJsonPointer)
+ self.assertEqual(operation.pointer_cls, CustomJsonPointer)
+ self.assertIsInstance(operation.pointer, CustomJsonPointer)
+
+ def test_operations_from_patch(self):
+ patch = jsonpatch.JsonPatch([
+ {'op': 'add', 'path': '/foo', 'value': [1, 2, 3]},
+ {'op': 'move', 'path': '/baz', 'from': '/foo'},
+ {'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'},
+ ], pointer_cls=CustomJsonPointer)
+ self.assertEqual(patch.apply({}), {'baz': [42]})
+ self.assertEqual(patch.pointer_cls, CustomJsonPointer)
+ self.assertTrue(patch._ops)
+ for op in patch._ops:
+ self.assertIsInstance(op.pointer, CustomJsonPointer)
+ self.assertEqual(op.pointer_cls, CustomJsonPointer)
+
+ def test_json_patch_with_prefix_pointer(self):
+ res = jsonpatch.apply_patch(
+ {'foo': {'bar': {}}}, [{'op': 'add', 'path': '/baz', 'value': 'qux'}],
+ pointer_cls=PrefixJsonPointer,
+ )
+ self.assertEqual(res, {'foo': {'bar': {'baz': 'qux'}}})
+
+
if __name__ == '__main__':
modules = ['jsonpatch']
@@ -826,6 +955,7 @@ if __name__ == '__main__':
suite.addTest(unittest.makeSuite(JsonPointerTests))
suite.addTest(unittest.makeSuite(JsonPatchCreationTest))
suite.addTest(unittest.makeSuite(UtilityMethodTests))
+ suite.addTest(unittest.makeSuite(CustomJsonPointerTests))
return suite