summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Kögl <stefan@skoegl.net>2020-12-01 20:52:44 +0100
committerGitHub <noreply@github.com>2020-12-01 20:52:44 +0100
commit57e4273effe9165838eda853a28208d637d70ca7 (patch)
tree4f87fa648f19c673febc1ed0c170fec6944be264
parentf3c46e8d37833eb3cb7ff5f914d8dac85450fafb (diff)
parent9310d48af5bfcde50f9b05fdd43deeafec11c805 (diff)
downloadpython-json-patch-57e4273effe9165838eda853a28208d637d70ca7.tar.gz
Merge pull request #118 from tzoiker/fix/json-patch-ops
Declare json-patch operations as a class-based attribute
-rw-r--r--jsonpatch.py395
-rwxr-xr-xtests.py28
2 files changed, 228 insertions, 195 deletions
diff --git a/jsonpatch.py b/jsonpatch.py
index 5522d50..a01a177 100644
--- a/jsonpatch.py
+++ b/jsonpatch.py
@@ -39,6 +39,11 @@ import copy
import functools
import json
import sys
+try:
+ from types import MappingProxyType
+except ImportError:
+ # Python < 3.3
+ MappingProxyType = dict
from jsonpointer import JsonPointer, JsonPointerException
@@ -170,201 +175,6 @@ def make_patch(src, dst, pointer_cls=JsonPointer):
return JsonPatch.from_diff(src, dst, pointer_cls=pointer_cls)
-class JsonPatch(object):
- json_dumper = staticmethod(json.dumps)
- json_loader = staticmethod(_jsonloads)
-
- """A JSON Patch is a list of Patch Operations.
-
- >>> patch = JsonPatch([
- ... {'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 = {}
- >>> result = patch.apply(doc)
- >>> expected = {'foo': 'bar', 'baz': [42]}
- >>> result == expected
- True
-
- JsonPatch object is iterable, so you can easily access each patch
- statement in a loop:
-
- >>> lpatch = list(patch)
- >>> expected = {'op': 'add', 'path': '/foo', 'value': 'bar'}
- >>> lpatch[0] == expected
- True
- >>> lpatch == patch.patch
- True
-
- Also JsonPatch could be converted directly to :class:`bool` if it contains
- any operation statements:
-
- >>> bool(patch)
- True
- >>> bool(JsonPatch([]))
- False
-
- This behavior is very handy with :func:`make_patch` to write more readable
- code:
-
- >>> old = {'foo': 'bar', 'numbers': [1, 3, 4, 8]}
- >>> new = {'baz': 'qux', 'numbers': [1, 4, 7]}
- >>> patch = make_patch(old, new)
- >>> if patch:
- ... # document have changed, do something useful
- ... patch.apply(old) #doctest: +ELLIPSIS
- {...}
- """
- def __init__(self, patch, pointer_cls=JsonPointer):
- self.patch = patch
- self.pointer_cls = pointer_cls
-
- self.operations = {
- 'remove': RemoveOperation,
- 'add': AddOperation,
- 'replace': ReplaceOperation,
- 'move': MoveOperation,
- 'test': TestOperation,
- 'copy': CopyOperation,
- }
-
- # Verify that the structure of the patch document
- # is correct by retrieving each patch element.
- # Much of the validation is done in the initializer
- # though some is delayed until the patch is applied.
- for op in self.patch:
- self._get_operation(op)
-
- def __str__(self):
- """str(self) -> self.to_string()"""
- return self.to_string()
-
- def __bool__(self):
- return bool(self.patch)
-
- __nonzero__ = __bool__
-
- def __iter__(self):
- return iter(self.patch)
-
- def __hash__(self):
- return hash(tuple(self._ops))
-
- def __eq__(self, other):
- if not isinstance(other, JsonPatch):
- return False
- return self._ops == other._ops
-
- def __ne__(self, other):
- return not(self == other)
-
- @classmethod
- 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, pointer_cls=pointer_cls)
-
- @classmethod
- 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.
-
- :param src: Data source document object.
- :type src: dict
-
- :param dst: Data source document object.
- :type dst: dict
-
- :param dumps: A function of one argument that produces a serialized
- 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]}
- >>> dst = {'baz': 'qux', 'numbers': [1, 4, 7]}
- >>> patch = JsonPatch.from_diff(src, dst)
- >>> new = patch.apply(src)
- >>> new == dst
- True
- """
- json_dumper = dumps or cls.json_dumper
- builder = DiffBuilder(json_dumper, pointer_cls=pointer_cls)
- builder._compare_values('', None, src, dst)
- ops = list(builder.execute())
- return cls(ops, pointer_cls=pointer_cls)
-
- def to_string(self, dumps=None):
- """Returns patch set as JSON string."""
- json_dumper = dumps or self.json_dumper
- return json_dumper(self.patch)
-
- @property
- def _ops(self):
- return tuple(map(self._get_operation, self.patch))
-
- def apply(self, obj, in_place=False):
- """Applies the patch to a given object.
-
- :param obj: Document object.
- :type obj: dict
-
- :param in_place: Tweaks the way how patch would be applied - directly to
- specified `obj` or to its copy.
- :type in_place: bool
-
- :return: Modified `obj`.
- """
-
- if not in_place:
- obj = copy.deepcopy(obj)
-
- for operation in self._ops:
- obj = operation.apply(obj)
-
- return obj
-
- def _get_operation(self, operation):
- if 'op' not in operation:
- raise InvalidJsonPatch("Operation does not contain 'op' member")
-
- op = operation['op']
-
- if not isinstance(op, basestring):
- raise InvalidJsonPatch("Operation must be a string")
-
- if op not in self.operations:
- raise InvalidJsonPatch("Unknown operation {0!r}".format(op))
-
- cls = self.operations[op]
- return cls(operation, pointer_cls=self.pointer_cls)
-
-
class PatchOperation(object):
"""A single operation inside a JSON Patch."""
@@ -681,6 +491,201 @@ class CopyOperation(PatchOperation):
return obj
+class JsonPatch(object):
+ json_dumper = staticmethod(json.dumps)
+ json_loader = staticmethod(_jsonloads)
+
+ operations = MappingProxyType({
+ 'remove': RemoveOperation,
+ 'add': AddOperation,
+ 'replace': ReplaceOperation,
+ 'move': MoveOperation,
+ 'test': TestOperation,
+ 'copy': CopyOperation,
+ })
+
+ """A JSON Patch is a list of Patch Operations.
+
+ >>> patch = JsonPatch([
+ ... {'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 = {}
+ >>> result = patch.apply(doc)
+ >>> expected = {'foo': 'bar', 'baz': [42]}
+ >>> result == expected
+ True
+
+ JsonPatch object is iterable, so you can easily access each patch
+ statement in a loop:
+
+ >>> lpatch = list(patch)
+ >>> expected = {'op': 'add', 'path': '/foo', 'value': 'bar'}
+ >>> lpatch[0] == expected
+ True
+ >>> lpatch == patch.patch
+ True
+
+ Also JsonPatch could be converted directly to :class:`bool` if it contains
+ any operation statements:
+
+ >>> bool(patch)
+ True
+ >>> bool(JsonPatch([]))
+ False
+
+ This behavior is very handy with :func:`make_patch` to write more readable
+ code:
+
+ >>> old = {'foo': 'bar', 'numbers': [1, 3, 4, 8]}
+ >>> new = {'baz': 'qux', 'numbers': [1, 4, 7]}
+ >>> patch = make_patch(old, new)
+ >>> if patch:
+ ... # document have changed, do something useful
+ ... patch.apply(old) #doctest: +ELLIPSIS
+ {...}
+ """
+ def __init__(self, patch, pointer_cls=JsonPointer):
+ self.patch = patch
+ self.pointer_cls = pointer_cls
+
+ # Verify that the structure of the patch document
+ # is correct by retrieving each patch element.
+ # Much of the validation is done in the initializer
+ # though some is delayed until the patch is applied.
+ for op in self.patch:
+ self._get_operation(op)
+
+ def __str__(self):
+ """str(self) -> self.to_string()"""
+ return self.to_string()
+
+ def __bool__(self):
+ return bool(self.patch)
+
+ __nonzero__ = __bool__
+
+ def __iter__(self):
+ return iter(self.patch)
+
+ def __hash__(self):
+ return hash(tuple(self._ops))
+
+ def __eq__(self, other):
+ if not isinstance(other, JsonPatch):
+ return False
+ return self._ops == other._ops
+
+ def __ne__(self, other):
+ return not(self == other)
+
+ @classmethod
+ 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, pointer_cls=pointer_cls)
+
+ @classmethod
+ 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.
+
+ :param src: Data source document object.
+ :type src: dict
+
+ :param dst: Data source document object.
+ :type dst: dict
+
+ :param dumps: A function of one argument that produces a serialized
+ 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]}
+ >>> dst = {'baz': 'qux', 'numbers': [1, 4, 7]}
+ >>> patch = JsonPatch.from_diff(src, dst)
+ >>> new = patch.apply(src)
+ >>> new == dst
+ True
+ """
+ json_dumper = dumps or cls.json_dumper
+ builder = DiffBuilder(json_dumper, pointer_cls=pointer_cls)
+ builder._compare_values('', None, src, dst)
+ ops = list(builder.execute())
+ return cls(ops, pointer_cls=pointer_cls)
+
+ def to_string(self, dumps=None):
+ """Returns patch set as JSON string."""
+ json_dumper = dumps or self.json_dumper
+ return json_dumper(self.patch)
+
+ @property
+ def _ops(self):
+ return tuple(map(self._get_operation, self.patch))
+
+ def apply(self, obj, in_place=False):
+ """Applies the patch to a given object.
+
+ :param obj: Document object.
+ :type obj: dict
+
+ :param in_place: Tweaks the way how patch would be applied - directly to
+ specified `obj` or to its copy.
+ :type in_place: bool
+
+ :return: Modified `obj`.
+ """
+
+ if not in_place:
+ obj = copy.deepcopy(obj)
+
+ for operation in self._ops:
+ obj = operation.apply(obj)
+
+ return obj
+
+ def _get_operation(self, operation):
+ if 'op' not in operation:
+ raise InvalidJsonPatch("Operation does not contain 'op' member")
+
+ op = operation['op']
+
+ if not isinstance(op, basestring):
+ raise InvalidJsonPatch("Operation must be a string")
+
+ if op not in self.operations:
+ raise InvalidJsonPatch("Unknown operation {0!r}".format(op))
+
+ cls = self.operations[op]
+ return cls(operation, pointer_cls=self.pointer_cls)
+
+
class DiffBuilder(object):
def __init__(self, dumps=json.dumps, pointer_cls=JsonPointer):
diff --git a/tests.py b/tests.py
index b5b7b9a..28fde9b 100755
--- a/tests.py
+++ b/tests.py
@@ -10,6 +10,11 @@ import unittest
import jsonpatch
import jsonpointer
import sys
+try:
+ from types import MappingProxyType
+except ImportError:
+ # Python < 3.3
+ MappingProxyType = dict
class ApplyPatchTestCase(unittest.TestCase):
@@ -938,6 +943,28 @@ class CustomJsonPointerTests(unittest.TestCase):
self.assertEqual(res, {'foo': {'bar': {'baz': 'qux'}}})
+class CustomOperationTests(unittest.TestCase):
+
+ def test_custom_operation(self):
+
+ class IdentityOperation(jsonpatch.PatchOperation):
+ def apply(self, obj):
+ return obj
+
+ class JsonPatch(jsonpatch.JsonPatch):
+ operations = MappingProxyType(
+ dict(
+ identity=IdentityOperation,
+ **jsonpatch.JsonPatch.operations
+ )
+ )
+
+ patch = JsonPatch([{'op': 'identity', 'path': '/'}])
+ self.assertIn('identity', patch.operations)
+ res = patch.apply({})
+ self.assertEqual(res, {})
+
+
if __name__ == '__main__':
modules = ['jsonpatch']
@@ -956,6 +983,7 @@ if __name__ == '__main__':
suite.addTest(unittest.makeSuite(JsonPatchCreationTest))
suite.addTest(unittest.makeSuite(UtilityMethodTests))
suite.addTest(unittest.makeSuite(CustomJsonPointerTests))
+ suite.addTest(unittest.makeSuite(CustomOperationTests))
return suite