3030# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3131#
3232
33+ """ Apply JSON-Patches (RFC 6902) """
34+
3335from __future__ import unicode_literals
3436
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
3645
3746# Will be parsed by setup.py to determine package metadata
3847__author__ = 'Stefan Kögl <stefan@skoegl.net>'
3948__version__ = '1.3'
4049__website__ = 'https://github.com/stefankoegl/python-json-patch'
4150__license__ = 'Modified BSD License'
4251
43- import copy
44- import sys
45- import operator
46- import collections
47-
48- import json
49-
50- import jsonpointer
5152
53+ # pylint: disable=E0611,W0404
5254if 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
5459
5560
56- JsonPointerException = jsonpointer .JsonPointerException
57-
5861class JsonPatchException (Exception ):
5962 """Base Json Patch exception"""
6063
@@ -67,22 +70,23 @@ class JsonPatchConflict(JsonPatchException):
6770 - etc.
6871 """
6972
73+
7074class JsonPatchTestFailed (JsonPatchException , AssertionError ):
7175 """ A Test operation failed """
7276
7377
7478def multidict (ordered_pairs ):
7579 """Convert duplicate keys values to lists."""
7680 # 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 )
8084
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+ )
8690
8791
8892def get_loadjson ():
@@ -94,9 +98,6 @@ def get_loadjson():
9498 function with object_pairs_hook set to multidict for Python versions that
9599 support the parameter. """
96100
97- import inspect
98- import functools
99-
100101 argspec = inspect .getargspec (json .load )
101102 if 'object_pairs_hook' not in argspec .args :
102103 return json .load
@@ -123,12 +124,14 @@ def apply_patch(doc, patch, in_place=False):
123124 :rtype: dict
124125
125126 >>> 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)
127129 >>> doc is not other
128130 True
129131 >>> other == {'foo': 'bar', 'baz': 'qux'}
130132 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'}
132135 True
133136 >>> doc == other
134137 True
@@ -140,6 +143,7 @@ def apply_patch(doc, patch, in_place=False):
140143 patch = JsonPatch (patch )
141144 return patch .apply (doc , in_place )
142145
146+
143147def make_patch (src , dst ):
144148 """Generates patch by comparing of two document objects. Actually is
145149 a proxy to :meth:`JsonPatch.from_diff` method.
@@ -230,18 +234,16 @@ def __bool__(self):
230234 def __iter__ (self ):
231235 return iter (self .patch )
232236
233-
234237 def __hash__ (self ):
235238 return hash (tuple (self ._ops ))
236239
237-
238240 def __eq__ (self , other ):
239241 if not isinstance (other , JsonPatch ):
240242 return False
243+ return self ._ops == other ._ops
241244
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 )
245247
246248 @classmethod
247249 def from_string (cls , patch_str ):
@@ -298,7 +300,9 @@ def compare_dict(path, src, dst):
298300 yield operation
299301 for key in dst :
300302 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 ]}
302306
303307 def compare_list (path , src , dst ):
304308 lsrc , ldst = len (src ), len (dst )
@@ -309,7 +313,9 @@ def compare_list(path, src, dst):
309313 if lsrc < ldst :
310314 for idx in range (lsrc , ldst ):
311315 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 ]}
313319 elif lsrc > ldst :
314320 for idx in reversed (range (ldst , lsrc )):
315321 yield {'op' : 'remove' , 'path' : '/' .join (path + [str (idx )])}
@@ -322,7 +328,7 @@ def to_string(self):
322328
323329 @property
324330 def _ops (self ):
325- return map (self ._get_operation , self .patch )
331+ return tuple ( map (self ._get_operation , self .patch ) )
326332
327333 def apply (self , obj , in_place = False ):
328334 """Applies the patch to given object.
@@ -355,36 +361,35 @@ def _get_operation(self, operation):
355361 raise JsonPatchException ("Operation must be a string" )
356362
357363 if op not in self .operations :
358- raise JsonPatchException ("Unknown operation '%s'" % op )
364+ raise JsonPatchException ("Unknown operation {0!r}" . format ( op ) )
359365
360366 cls = self .operations [op ]
361367 return cls (operation )
362368
363369
364-
365370class PatchOperation (object ):
366371 """A single operation inside a JSON Patch."""
367372
368373 def __init__ (self , operation ):
369374 self .location = operation ['path' ]
370- self .pointer = jsonpointer . JsonPointer (self .location )
375+ self .pointer = JsonPointer (self .location )
371376 self .operation = operation
372377
373378 def apply (self , obj ):
374379 """Abstract method that applies patch operation to specified object."""
375380 raise NotImplementedError ('should implement patch operation.' )
376381
377-
378382 def __hash__ (self ):
379383 return hash (frozenset (self .operation .items ()))
380384
381-
382385 def __eq__ (self , other ):
383386 if not isinstance (other , PatchOperation ):
384387 return False
385-
386388 return self .operation == other .operation
387389
390+ def __ne__ (self , other ):
391+ return not (self == other )
392+
388393
389394class RemoveOperation (PatchOperation ):
390395 """Removes an object property or an array element."""
@@ -406,30 +411,25 @@ def apply(self, obj):
406411 value = self .operation ["value" ]
407412 subobj , part = self .pointer .to_last (obj )
408413
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-
414414 if isinstance (subobj , list ):
415-
416415 if part == '-' :
417- subobj .append (value )
416+ subobj .append (value ) # pylint: disable=E1103
418417
419418 elif part > len (subobj ) or part < 0 :
420419 raise JsonPatchConflict ("can't insert outside of list" )
421420
422421 else :
423- subobj .insert (part , value )
422+ subobj .insert (part , value ) # pylint: disable=E1103
424423
425424 elif isinstance (subobj , dict ):
426425 if part is None :
427- # we're replacing the root
428- obj = value
429-
426+ obj = value # we're replacing the root
430427 else :
431428 subobj [part ] = value
432429
430+ else :
431+ raise TypeError ("invalid document type {0}" .format (type (subobj )))
432+
433433 return obj
434434
435435
@@ -440,11 +440,6 @@ def apply(self, obj):
440440 value = self .operation ["value" ]
441441 subobj , part = self .pointer .to_last (obj )
442442
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-
448443 if part is None :
449444 return value
450445
@@ -454,8 +449,10 @@ def apply(self, obj):
454449
455450 elif isinstance (subobj , dict ):
456451 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 )))
459456
460457 subobj [part ] = value
461458 return obj
@@ -465,15 +462,24 @@ class MoveOperation(PatchOperation):
465462 """Moves an object property or an array element to new location."""
466463
467464 def apply (self , obj ):
468- from_ptr = jsonpointer . JsonPointer (self .operation ['from' ])
465+ from_ptr = JsonPointer (self .operation ['from' ])
469466 subobj , part = from_ptr .to_last (obj )
470467 value = subobj [part ]
471468
472469 if self .pointer .contains (from_ptr ):
473470 raise JsonPatchException ('Cannot move values into its own children' )
474471
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+
477483 return obj
478484
479485
@@ -487,14 +493,15 @@ def apply(self, obj):
487493 val = subobj
488494 else :
489495 val = self .pointer .walk (subobj , part )
490-
491496 except JsonPointerException as ex :
492497 raise JsonPatchTestFailed (str (ex ))
493498
494499 if 'value' in self .operation :
495500 value = self .operation ['value' ]
496501 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 )))
498505
499506 return obj
500507
@@ -503,8 +510,14 @@ class CopyOperation(PatchOperation):
503510 """ Copies an object property or an array element to a new location """
504511
505512 def apply (self , obj ):
506- from_ptr = jsonpointer . JsonPointer (self .operation ['from' ])
513+ from_ptr = JsonPointer (self .operation ['from' ])
507514 subobj , part = from_ptr .to_last (obj )
508515 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+
510523 return obj
0 commit comments