summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Lyder Jacobsen <christian@yocuda.com>2020-03-06 11:41:38 +0100
committerChristian Lyder Jacobsen <christian@yocuda.com>2020-03-06 12:07:24 +0100
commit0167d345ee9d7ef0f74b947ec3a7ea94def178be (patch)
tree34b4358b9b96ac73ca06c5d741bd7c344cb164a8
parente99d178396f69f8891a62e21434c2783b76146b2 (diff)
downloadpython-json-patch-0167d345ee9d7ef0f74b947ec3a7ea94def178be.tar.gz
Subclassing can override json dumper and loader
Additionally: * from_string gets a loads parameter * to_string gets a dumps_parameter * documentation added * added more tests
-rw-r--r--doc/tutorial.rst52
-rw-r--r--jsonpatch.py22
-rwxr-xr-xtests.py59
3 files changed, 118 insertions, 15 deletions
diff --git a/doc/tutorial.rst b/doc/tutorial.rst
index 538cd0e..0bb1a9c 100644
--- a/doc/tutorial.rst
+++ b/doc/tutorial.rst
@@ -67,3 +67,55 @@ explicitly.
# or from a list
>>> patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}]
>>> res = jsonpatch.apply_patch(obj, patch)
+
+
+Dealing with Custom Types
+-------------------------
+
+Custom JSON dump and load functions can be used to support custom types such as
+`decimal.Decimal`. The following examples shows how the
+`simplejson <https://simplejson.readthedocs.io/>`_ package, which has native
+support for Python's ``Decimal`` type, can be used to create a custom
+``JsonPatch`` subclass with ``Decimal`` support:
+
+.. code-block:: python
+
+ >>> import decimal
+ >>> import simplejson
+
+ >>> class DecimalJsonPatch(jsonpatch.JsonPatch):
+ @staticmethod
+ def json_dumper(obj):
+ return simplejson.dumps(obj)
+
+ @staticmethod
+ def json_loader(obj):
+ return simplejson.loads(obj, use_decimal=True,
+ object_pairs_hook=jsonpatch.multidict)
+
+ >>> src = {}
+ >>> dst = {'bar': decimal.Decimal('1.10')}
+ >>> patch = DecimalJsonPatch.from_diff(src, dst)
+ >>> doc = {'foo': 1}
+ >>> result = patch.apply(doc)
+ {'foo': 1, 'bar': Decimal('1.10')}
+
+Instead of subclassing it is also possible to pass a dump function to
+``from_diff``:
+
+ >>> patch = jsonpatch.JsonPatch.from_diff(src, dst, dumps=simplejson.dumps)
+
+a dumps function to ``to_string``:
+
+ >>> serialized_patch = patch.to_string(dumps=simplejson.dumps)
+ '[{"op": "add", "path": "/bar", "value": 1.10}]'
+
+and load function to ``from_string``:
+
+ >>> import functools
+ >>> loads = functools.partial(simplejson.loads, use_decimal=True,
+ object_pairs_hook=jsonpatch.multidict)
+ >>> patch.from_string(serialized_patch, loads=loads)
+ >>> doc = {'foo': 1}
+ >>> result = patch.apply(doc)
+ {'foo': 1, 'bar': Decimal('1.10')}
diff --git a/jsonpatch.py b/jsonpatch.py
index ebaa8e3..ce8a89f 100644
--- a/jsonpatch.py
+++ b/jsonpatch.py
@@ -165,6 +165,9 @@ def make_patch(src, dst):
class JsonPatch(object):
+ json_dumper = staticmethod(json.dumps)
+ json_loader = staticmethod(_jsonloads)
+
"""A JSON Patch is a list of Patch Operations.
>>> patch = JsonPatch([
@@ -246,19 +249,23 @@ class JsonPatch(object):
return not(self == other)
@classmethod
- def from_string(cls, patch_str):
+ def from_string(cls, patch_str, loads=None):
"""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
:return: :class:`JsonPatch` instance.
"""
- patch = _jsonloads(patch_str)
+ json_loader = loads or cls.json_loader
+ patch = json_loader(patch_str)
return cls(patch)
@classmethod
- def from_diff(cls, src, dst, optimization=True, dumps=json.dumps):
+ def from_diff(cls, src, dst, optimization=True, dumps=None):
"""Creates JsonPatch instance based on comparing of two document
objects. Json patch would be created for `src` argument against `dst`
one.
@@ -282,15 +289,16 @@ class JsonPatch(object):
>>> new == dst
True
"""
-
- builder = DiffBuilder(dumps)
+ json_dumper = dumps or cls.json_dumper
+ builder = DiffBuilder(json_dumper)
builder._compare_values('', None, src, dst)
ops = list(builder.execute())
return cls(ops)
- def to_string(self):
+ def to_string(self, dumps=None):
"""Returns patch set as JSON string."""
- return json.dumps(self.patch)
+ json_dumper = dumps or self.json_dumper
+ return json_dumper(self.patch)
@property
def _ops(self):
diff --git a/tests.py b/tests.py
index 8837bfa..a843b35 100755
--- a/tests.py
+++ b/tests.py
@@ -268,6 +268,34 @@ class EqualityTestCase(unittest.TestCase):
self.assertEqual(json.dumps(patch_obj), patch.to_string())
+def custom_types_dumps(obj):
+ def default(obj):
+ if isinstance(obj, decimal.Decimal):
+ return {'__decimal__': str(obj)}
+ raise TypeError('Unknown type')
+
+ return json.dumps(obj, default=default)
+
+
+def custom_types_loads(obj):
+ def as_decimal(dct):
+ if '__decimal__' in dct:
+ return decimal.Decimal(dct['__decimal__'])
+ return dct
+
+ return json.loads(obj, object_hook=as_decimal)
+
+
+class CustomTypesJsonPatch(jsonpatch.JsonPatch):
+ @staticmethod
+ def json_dumper(obj):
+ return custom_types_dumps(obj)
+
+ @staticmethod
+ def json_loader(obj):
+ return custom_types_loads(obj)
+
+
class MakePatchTestCase(unittest.TestCase):
def test_apply_patch_to_copy(self):
@@ -446,18 +474,33 @@ class MakePatchTestCase(unittest.TestCase):
self.assertEqual(res, dst)
self.assertIsInstance(res['A'], float)
- def test_custom_types(self):
- def default(obj):
- if isinstance(obj, decimal.Decimal):
- return str(obj)
- raise TypeError('Unknown type')
+ def test_custom_types_diff(self):
+ old = {'value': decimal.Decimal('1.0')}
+ new = {'value': decimal.Decimal('1.00')}
+ generated_patch = jsonpatch.JsonPatch.from_diff(
+ old, new, dumps=custom_types_dumps)
+ str_patch = generated_patch.to_string(dumps=custom_types_dumps)
+ loaded_patch = jsonpatch.JsonPatch.from_string(
+ str_patch, loads=custom_types_loads)
+ self.assertEqual(generated_patch, loaded_patch)
+ new_from_patch = jsonpatch.apply_patch(old, generated_patch)
+ self.assertEqual(new, new_from_patch)
- def dumps(obj):
- return json.dumps(obj, default=default)
+ def test_custom_types_subclass(self):
+ old = {'value': decimal.Decimal('1.0')}
+ new = {'value': decimal.Decimal('1.00')}
+ generated_patch = CustomTypesJsonPatch.from_diff(old, new)
+ str_patch = generated_patch.to_string()
+ loaded_patch = CustomTypesJsonPatch.from_string(str_patch)
+ self.assertEqual(generated_patch, loaded_patch)
+ new_from_patch = jsonpatch.apply_patch(old, loaded_patch)
+ self.assertEqual(new, new_from_patch)
+ def test_custom_types_subclass_load(self):
old = {'value': decimal.Decimal('1.0')}
new = {'value': decimal.Decimal('1.00')}
- patch = jsonpatch.JsonPatch.from_diff(old, new, dumps=dumps)
+ patch = CustomTypesJsonPatch.from_string(
+ '[{"op": "replace", "path": "/value", "value": {"__decimal__": "1.00"}}]')
new_from_patch = jsonpatch.apply_patch(old, patch)
self.assertEqual(new, new_from_patch)