Skip to content

Commit 0167d34

Browse files
committed
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
1 parent e99d178 commit 0167d34

File tree

3 files changed

+118
-15
lines changed

3 files changed

+118
-15
lines changed

doc/tutorial.rst

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,55 @@ explicitly.
6767
# or from a list
6868
>>> patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}]
6969
>>> res = jsonpatch.apply_patch(obj, patch)
70+
71+
72+
Dealing with Custom Types
73+
-------------------------
74+
75+
Custom JSON dump and load functions can be used to support custom types such as
76+
`decimal.Decimal`. The following examples shows how the
77+
`simplejson <https://simplejson.readthedocs.io/>`_ package, which has native
78+
support for Python's ``Decimal`` type, can be used to create a custom
79+
``JsonPatch`` subclass with ``Decimal`` support:
80+
81+
.. code-block:: python
82+
83+
>>> import decimal
84+
>>> import simplejson
85+
86+
>>> class DecimalJsonPatch(jsonpatch.JsonPatch):
87+
@staticmethod
88+
def json_dumper(obj):
89+
return simplejson.dumps(obj)
90+
91+
@staticmethod
92+
def json_loader(obj):
93+
return simplejson.loads(obj, use_decimal=True,
94+
object_pairs_hook=jsonpatch.multidict)
95+
96+
>>> src = {}
97+
>>> dst = {'bar': decimal.Decimal('1.10')}
98+
>>> patch = DecimalJsonPatch.from_diff(src, dst)
99+
>>> doc = {'foo': 1}
100+
>>> result = patch.apply(doc)
101+
{'foo': 1, 'bar': Decimal('1.10')}
102+
103+
Instead of subclassing it is also possible to pass a dump function to
104+
``from_diff``:
105+
106+
>>> patch = jsonpatch.JsonPatch.from_diff(src, dst, dumps=simplejson.dumps)
107+
108+
a dumps function to ``to_string``:
109+
110+
>>> serialized_patch = patch.to_string(dumps=simplejson.dumps)
111+
'[{"op": "add", "path": "/bar", "value": 1.10}]'
112+
113+
and load function to ``from_string``:
114+
115+
>>> import functools
116+
>>> loads = functools.partial(simplejson.loads, use_decimal=True,
117+
object_pairs_hook=jsonpatch.multidict)
118+
>>> patch.from_string(serialized_patch, loads=loads)
119+
>>> doc = {'foo': 1}
120+
>>> result = patch.apply(doc)
121+
{'foo': 1, 'bar': Decimal('1.10')}

jsonpatch.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ def make_patch(src, dst):
165165

166166

167167
class JsonPatch(object):
168+
json_dumper = staticmethod(json.dumps)
169+
json_loader = staticmethod(_jsonloads)
170+
168171
"""A JSON Patch is a list of Patch Operations.
169172
170173
>>> patch = JsonPatch([
@@ -246,19 +249,23 @@ def __ne__(self, other):
246249
return not(self == other)
247250

248251
@classmethod
249-
def from_string(cls, patch_str):
252+
def from_string(cls, patch_str, loads=None):
250253
"""Creates JsonPatch instance from string source.
251254
252255
:param patch_str: JSON patch as raw string.
253256
:type patch_str: str
257+
:param loads: A function of one argument that loads a serialized
258+
JSON string.
259+
:type loads: function
254260
255261
:return: :class:`JsonPatch` instance.
256262
"""
257-
patch = _jsonloads(patch_str)
263+
json_loader = loads or cls.json_loader
264+
patch = json_loader(patch_str)
258265
return cls(patch)
259266

260267
@classmethod
261-
def from_diff(cls, src, dst, optimization=True, dumps=json.dumps):
268+
def from_diff(cls, src, dst, optimization=True, dumps=None):
262269
"""Creates JsonPatch instance based on comparing of two document
263270
objects. Json patch would be created for `src` argument against `dst`
264271
one.
@@ -282,15 +289,16 @@ def from_diff(cls, src, dst, optimization=True, dumps=json.dumps):
282289
>>> new == dst
283290
True
284291
"""
285-
286-
builder = DiffBuilder(dumps)
292+
json_dumper = dumps or cls.json_dumper
293+
builder = DiffBuilder(json_dumper)
287294
builder._compare_values('', None, src, dst)
288295
ops = list(builder.execute())
289296
return cls(ops)
290297

291-
def to_string(self):
298+
def to_string(self, dumps=None):
292299
"""Returns patch set as JSON string."""
293-
return json.dumps(self.patch)
300+
json_dumper = dumps or self.json_dumper
301+
return json_dumper(self.patch)
294302

295303
@property
296304
def _ops(self):

tests.py

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,34 @@ def test_str(self):
268268
self.assertEqual(json.dumps(patch_obj), patch.to_string())
269269

270270

271+
def custom_types_dumps(obj):
272+
def default(obj):
273+
if isinstance(obj, decimal.Decimal):
274+
return {'__decimal__': str(obj)}
275+
raise TypeError('Unknown type')
276+
277+
return json.dumps(obj, default=default)
278+
279+
280+
def custom_types_loads(obj):
281+
def as_decimal(dct):
282+
if '__decimal__' in dct:
283+
return decimal.Decimal(dct['__decimal__'])
284+
return dct
285+
286+
return json.loads(obj, object_hook=as_decimal)
287+
288+
289+
class CustomTypesJsonPatch(jsonpatch.JsonPatch):
290+
@staticmethod
291+
def json_dumper(obj):
292+
return custom_types_dumps(obj)
293+
294+
@staticmethod
295+
def json_loader(obj):
296+
return custom_types_loads(obj)
297+
298+
271299
class MakePatchTestCase(unittest.TestCase):
272300

273301
def test_apply_patch_to_copy(self):
@@ -446,18 +474,33 @@ def test_issue103(self):
446474
self.assertEqual(res, dst)
447475
self.assertIsInstance(res['A'], float)
448476

449-
def test_custom_types(self):
450-
def default(obj):
451-
if isinstance(obj, decimal.Decimal):
452-
return str(obj)
453-
raise TypeError('Unknown type')
477+
def test_custom_types_diff(self):
478+
old = {'value': decimal.Decimal('1.0')}
479+
new = {'value': decimal.Decimal('1.00')}
480+
generated_patch = jsonpatch.JsonPatch.from_diff(
481+
old, new, dumps=custom_types_dumps)
482+
str_patch = generated_patch.to_string(dumps=custom_types_dumps)
483+
loaded_patch = jsonpatch.JsonPatch.from_string(
484+
str_patch, loads=custom_types_loads)
485+
self.assertEqual(generated_patch, loaded_patch)
486+
new_from_patch = jsonpatch.apply_patch(old, generated_patch)
487+
self.assertEqual(new, new_from_patch)
454488

455-
def dumps(obj):
456-
return json.dumps(obj, default=default)
489+
def test_custom_types_subclass(self):
490+
old = {'value': decimal.Decimal('1.0')}
491+
new = {'value': decimal.Decimal('1.00')}
492+
generated_patch = CustomTypesJsonPatch.from_diff(old, new)
493+
str_patch = generated_patch.to_string()
494+
loaded_patch = CustomTypesJsonPatch.from_string(str_patch)
495+
self.assertEqual(generated_patch, loaded_patch)
496+
new_from_patch = jsonpatch.apply_patch(old, loaded_patch)
497+
self.assertEqual(new, new_from_patch)
457498

499+
def test_custom_types_subclass_load(self):
458500
old = {'value': decimal.Decimal('1.0')}
459501
new = {'value': decimal.Decimal('1.00')}
460-
patch = jsonpatch.JsonPatch.from_diff(old, new, dumps=dumps)
502+
patch = CustomTypesJsonPatch.from_string(
503+
'[{"op": "replace", "path": "/value", "value": {"__decimal__": "1.00"}}]')
461504
new_from_patch = jsonpatch.apply_patch(old, patch)
462505
self.assertEqual(new, new_from_patch)
463506

0 commit comments

Comments
 (0)