30
30
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
31
#
32
32
33
+ """ Apply JSON-Patches (RFC 6902) """
34
+
33
35
from __future__ import unicode_literals
34
36
35
- """ Apply JSON-Patches (RFC 6902) """
37
+ import collections
38
+ import copy
39
+ import functools
40
+ import inspect
41
+ import json
42
+ import sys
43
+
44
+ from jsonpointer import JsonPointer , JsonPointerException
36
45
37
46
# Will be parsed by setup.py to determine package metadata
38
47
__author__ = 'Stefan Kögl <[email protected] >'
39
48
__version__ = '1.3'
40
49
__website__ = 'https://github.com/stefankoegl/python-json-patch'
41
50
__license__ = 'Modified BSD License'
42
51
43
- import copy
44
- import sys
45
- import operator
46
- import collections
47
-
48
- import json
49
-
50
- import jsonpointer
51
52
53
+ # pylint: disable=E0611,W0404
52
54
if sys .version_info >= (3 , 0 ):
53
- basestring = (bytes , str )
55
+ basestring = (bytes , str ) # pylint: disable=C0103,W0622
56
+ from itertools import zip_longest
57
+ else :
58
+ from itertools import izip_longest as zip_longest
54
59
55
60
56
- JsonPointerException = jsonpointer .JsonPointerException
57
-
58
61
class JsonPatchException (Exception ):
59
62
"""Base Json Patch exception"""
60
63
@@ -67,22 +70,23 @@ class JsonPatchConflict(JsonPatchException):
67
70
- etc.
68
71
"""
69
72
73
+
70
74
class JsonPatchTestFailed (JsonPatchException , AssertionError ):
71
75
""" A Test operation failed """
72
76
73
77
74
78
def multidict (ordered_pairs ):
75
79
"""Convert duplicate keys values to lists."""
76
80
# read all values into lists
77
- d = collections .defaultdict (list )
78
- for k , v in ordered_pairs :
79
- d [ k ].append (v )
81
+ mdict = collections .defaultdict (list )
82
+ for key , value in ordered_pairs :
83
+ mdict [ key ].append (value )
80
84
81
- # unpack lists that have only 1 item
82
- for k , v in d . items ():
83
- if len (v ) == 1 :
84
- d [ k ] = v [ 0 ]
85
- return dict ( d )
85
+ return dict (
86
+ # unpack lists that have only 1 item
87
+ ( key , values [ 0 ] if len (values ) == 1 else values )
88
+ for key , values in mdict . items ()
89
+ )
86
90
87
91
88
92
def get_loadjson ():
@@ -94,9 +98,6 @@ def get_loadjson():
94
98
function with object_pairs_hook set to multidict for Python versions that
95
99
support the parameter. """
96
100
97
- import inspect
98
- import functools
99
-
100
101
argspec = inspect .getargspec (json .load )
101
102
if 'object_pairs_hook' not in argspec .args :
102
103
return json .load
@@ -123,12 +124,14 @@ def apply_patch(doc, patch, in_place=False):
123
124
:rtype: dict
124
125
125
126
>>> doc = {'foo': 'bar'}
126
- >>> other = apply_patch(doc, [{'op': 'add', 'path': '/baz', 'value': 'qux'}])
127
+ >>> patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}]
128
+ >>> other = apply_patch(doc, patch)
127
129
>>> doc is not other
128
130
True
129
131
>>> other == {'foo': 'bar', 'baz': 'qux'}
130
132
True
131
- >>> apply_patch(doc, [{'op': 'add', 'path': '/baz', 'value': 'qux'}], in_place=True) == {'foo': 'bar', 'baz': 'qux'}
133
+ >>> patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}]
134
+ >>> apply_patch(doc, patch, in_place=True) == {'foo': 'bar', 'baz': 'qux'}
132
135
True
133
136
>>> doc == other
134
137
True
@@ -140,6 +143,7 @@ def apply_patch(doc, patch, in_place=False):
140
143
patch = JsonPatch (patch )
141
144
return patch .apply (doc , in_place )
142
145
146
+
143
147
def make_patch (src , dst ):
144
148
"""Generates patch by comparing of two document objects. Actually is
145
149
a proxy to :meth:`JsonPatch.from_diff` method.
@@ -230,18 +234,16 @@ def __bool__(self):
230
234
def __iter__ (self ):
231
235
return iter (self .patch )
232
236
233
-
234
237
def __hash__ (self ):
235
238
return hash (tuple (self ._ops ))
236
239
237
-
238
240
def __eq__ (self , other ):
239
241
if not isinstance (other , JsonPatch ):
240
242
return False
243
+ return self ._ops == other ._ops
241
244
242
- return len (list (self ._ops )) == len (list (other ._ops )) and \
243
- all (map (operator .eq , self ._ops , other ._ops ))
244
-
245
+ def __ne__ (self , other ):
246
+ return not (self == other )
245
247
246
248
@classmethod
247
249
def from_string (cls , patch_str ):
@@ -298,7 +300,9 @@ def compare_dict(path, src, dst):
298
300
yield operation
299
301
for key in dst :
300
302
if key not in src :
301
- yield {'op' : 'add' , 'path' : '/' .join (path + [key ]), 'value' : dst [key ]}
303
+ yield {'op' : 'add' ,
304
+ 'path' : '/' .join (path + [key ]),
305
+ 'value' : dst [key ]}
302
306
303
307
def compare_list (path , src , dst ):
304
308
lsrc , ldst = len (src ), len (dst )
@@ -309,7 +313,9 @@ def compare_list(path, src, dst):
309
313
if lsrc < ldst :
310
314
for idx in range (lsrc , ldst ):
311
315
current = path + [str (idx )]
312
- yield {'op' : 'add' , 'path' : '/' .join (current ), 'value' : dst [idx ]}
316
+ yield {'op' : 'add' ,
317
+ 'path' : '/' .join (current ),
318
+ 'value' : dst [idx ]}
313
319
elif lsrc > ldst :
314
320
for idx in reversed (range (ldst , lsrc )):
315
321
yield {'op' : 'remove' , 'path' : '/' .join (path + [str (idx )])}
@@ -322,7 +328,7 @@ def to_string(self):
322
328
323
329
@property
324
330
def _ops (self ):
325
- return map (self ._get_operation , self .patch )
331
+ return tuple ( map (self ._get_operation , self .patch ) )
326
332
327
333
def apply (self , obj , in_place = False ):
328
334
"""Applies the patch to given object.
@@ -355,36 +361,35 @@ def _get_operation(self, operation):
355
361
raise JsonPatchException ("Operation must be a string" )
356
362
357
363
if op not in self .operations :
358
- raise JsonPatchException ("Unknown operation '%s'" % op )
364
+ raise JsonPatchException ("Unknown operation {0!r}" . format ( op ) )
359
365
360
366
cls = self .operations [op ]
361
367
return cls (operation )
362
368
363
369
364
-
365
370
class PatchOperation (object ):
366
371
"""A single operation inside a JSON Patch."""
367
372
368
373
def __init__ (self , operation ):
369
374
self .location = operation ['path' ]
370
- self .pointer = jsonpointer . JsonPointer (self .location )
375
+ self .pointer = JsonPointer (self .location )
371
376
self .operation = operation
372
377
373
378
def apply (self , obj ):
374
379
"""Abstract method that applies patch operation to specified object."""
375
380
raise NotImplementedError ('should implement patch operation.' )
376
381
377
-
378
382
def __hash__ (self ):
379
383
return hash (frozenset (self .operation .items ()))
380
384
381
-
382
385
def __eq__ (self , other ):
383
386
if not isinstance (other , PatchOperation ):
384
387
return False
385
-
386
388
return self .operation == other .operation
387
389
390
+ def __ne__ (self , other ):
391
+ return not (self == other )
392
+
388
393
389
394
class RemoveOperation (PatchOperation ):
390
395
"""Removes an object property or an array element."""
@@ -406,30 +411,25 @@ def apply(self, obj):
406
411
value = self .operation ["value" ]
407
412
subobj , part = self .pointer .to_last (obj )
408
413
409
- # type is already checked in to_last(), so we assert here
410
- # for consistency
411
- assert isinstance (subobj , list ) or isinstance (subobj , dict ), \
412
- "invalid document type %s" (type (doc ),)
413
-
414
414
if isinstance (subobj , list ):
415
-
416
415
if part == '-' :
417
- subobj .append (value )
416
+ subobj .append (value ) # pylint: disable=E1103
418
417
419
418
elif part > len (subobj ) or part < 0 :
420
419
raise JsonPatchConflict ("can't insert outside of list" )
421
420
422
421
else :
423
- subobj .insert (part , value )
422
+ subobj .insert (part , value ) # pylint: disable=E1103
424
423
425
424
elif isinstance (subobj , dict ):
426
425
if part is None :
427
- # we're replacing the root
428
- obj = value
429
-
426
+ obj = value # we're replacing the root
430
427
else :
431
428
subobj [part ] = value
432
429
430
+ else :
431
+ raise TypeError ("invalid document type {0}" .format (type (subobj )))
432
+
433
433
return obj
434
434
435
435
@@ -440,11 +440,6 @@ def apply(self, obj):
440
440
value = self .operation ["value" ]
441
441
subobj , part = self .pointer .to_last (obj )
442
442
443
- # type is already checked in to_last(), so we assert here
444
- # for consistency
445
- assert isinstance (subobj , list ) or isinstance (subobj , dict ), \
446
- "invalid document type %s" (type (doc ),)
447
-
448
443
if part is None :
449
444
return value
450
445
@@ -454,8 +449,10 @@ def apply(self, obj):
454
449
455
450
elif isinstance (subobj , dict ):
456
451
if not part in subobj :
457
- raise JsonPatchConflict ("can't replace non-existant object '%s'"
458
- "" % part )
452
+ msg = "can't replace non-existent object '{0}'" .format (part )
453
+ raise JsonPatchConflict (msg )
454
+ else :
455
+ raise TypeError ("invalid document type {0}" .format (type (subobj )))
459
456
460
457
subobj [part ] = value
461
458
return obj
@@ -465,15 +462,24 @@ class MoveOperation(PatchOperation):
465
462
"""Moves an object property or an array element to new location."""
466
463
467
464
def apply (self , obj ):
468
- from_ptr = jsonpointer . JsonPointer (self .operation ['from' ])
465
+ from_ptr = JsonPointer (self .operation ['from' ])
469
466
subobj , part = from_ptr .to_last (obj )
470
467
value = subobj [part ]
471
468
472
469
if self .pointer .contains (from_ptr ):
473
470
raise JsonPatchException ('Cannot move values into its own children' )
474
471
475
- obj = RemoveOperation ({'op' : 'remove' , 'path' : self .operation ['from' ]}).apply (obj )
476
- obj = AddOperation ({'op' : 'add' , 'path' : self .location , 'value' : value }).apply (obj )
472
+ obj = RemoveOperation ({
473
+ 'op' : 'remove' ,
474
+ 'path' : self .operation ['from' ]
475
+ }).apply (obj )
476
+
477
+ obj = AddOperation ({
478
+ 'op' : 'add' ,
479
+ 'path' : self .location ,
480
+ 'value' : value
481
+ }).apply (obj )
482
+
477
483
return obj
478
484
479
485
@@ -487,14 +493,15 @@ def apply(self, obj):
487
493
val = subobj
488
494
else :
489
495
val = self .pointer .walk (subobj , part )
490
-
491
496
except JsonPointerException as ex :
492
497
raise JsonPatchTestFailed (str (ex ))
493
498
494
499
if 'value' in self .operation :
495
500
value = self .operation ['value' ]
496
501
if val != value :
497
- raise JsonPatchTestFailed ('%s is not equal to tested value %s (types %s and %s)' % (val , value , type (val ), type (value )))
502
+ msg = '{0} ({1}) is not equal to tested value {2} ({3})'
503
+ raise JsonPatchTestFailed (msg .format (val , type (val ),
504
+ value , type (value )))
498
505
499
506
return obj
500
507
@@ -503,8 +510,14 @@ class CopyOperation(PatchOperation):
503
510
""" Copies an object property or an array element to a new location """
504
511
505
512
def apply (self , obj ):
506
- from_ptr = jsonpointer . JsonPointer (self .operation ['from' ])
513
+ from_ptr = JsonPointer (self .operation ['from' ])
507
514
subobj , part = from_ptr .to_last (obj )
508
515
value = copy .deepcopy (subobj [part ])
509
- obj = AddOperation ({'op' : 'add' , 'path' : self .location , 'value' : value }).apply (obj )
516
+
517
+ obj = AddOperation ({
518
+ 'op' : 'add' ,
519
+ 'path' : self .location ,
520
+ 'value' : value
521
+ }).apply (obj )
522
+
510
523
return obj
0 commit comments