davidtran999 commited on
Commit
a58d8b2
·
verified ·
1 Parent(s): 4ea4bb7

Upload backend/venv/lib/python3.10/site-packages/jsonpatch.py with huggingface_hub

Browse files
backend/venv/lib/python3.10/site-packages/jsonpatch.py ADDED
@@ -0,0 +1,931 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # python-json-patch - An implementation of the JSON Patch format
4
+ # https://github.com/stefankoegl/python-json-patch
5
+ #
6
+ # Copyright (c) 2011 Stefan Kögl <stefan@skoegl.net>
7
+ # All rights reserved.
8
+ #
9
+ # Redistribution and use in source and binary forms, with or without
10
+ # modification, are permitted provided that the following conditions
11
+ # are met:
12
+ #
13
+ # 1. Redistributions of source code must retain the above copyright
14
+ # notice, this list of conditions and the following disclaimer.
15
+ # 2. Redistributions in binary form must reproduce the above copyright
16
+ # notice, this list of conditions and the following disclaimer in the
17
+ # documentation and/or other materials provided with the distribution.
18
+ # 3. The name of the author may not be used to endorse or promote products
19
+ # derived from this software without specific prior written permission.
20
+ #
21
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
22
+ # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
23
+ # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
24
+ # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
25
+ # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
26
+ # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
30
+ # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
+ #
32
+
33
+ """ Apply JSON-Patches (RFC 6902) """
34
+
35
+ from __future__ import unicode_literals
36
+
37
+ import collections
38
+ import copy
39
+ import functools
40
+ import json
41
+ import sys
42
+
43
+ try:
44
+ from collections.abc import Sequence
45
+ except ImportError: # Python 3
46
+ from collections import Sequence
47
+
48
+ try:
49
+ from types import MappingProxyType
50
+ except ImportError:
51
+ # Python < 3.3
52
+ MappingProxyType = dict
53
+
54
+ from jsonpointer import JsonPointer, JsonPointerException
55
+
56
+
57
+ _ST_ADD = 0
58
+ _ST_REMOVE = 1
59
+
60
+
61
+ try:
62
+ from collections.abc import MutableMapping, MutableSequence
63
+
64
+ except ImportError:
65
+ from collections import MutableMapping, MutableSequence
66
+ str = unicode
67
+
68
+ # Will be parsed by setup.py to determine package metadata
69
+ __author__ = 'Stefan Kögl <stefan@skoegl.net>'
70
+ __version__ = '1.33'
71
+ __website__ = 'https://github.com/stefankoegl/python-json-patch'
72
+ __license__ = 'Modified BSD License'
73
+
74
+
75
+ # pylint: disable=E0611,W0404
76
+ if sys.version_info >= (3, 0):
77
+ basestring = (bytes, str) # pylint: disable=C0103,W0622
78
+
79
+
80
+ class JsonPatchException(Exception):
81
+ """Base Json Patch exception"""
82
+
83
+
84
+ class InvalidJsonPatch(JsonPatchException):
85
+ """ Raised if an invalid JSON Patch is created """
86
+
87
+
88
+ class JsonPatchConflict(JsonPatchException):
89
+ """Raised if patch could not be applied due to conflict situation such as:
90
+ - attempt to add object key when it already exists;
91
+ - attempt to operate with nonexistence object key;
92
+ - attempt to insert value to array at position beyond its size;
93
+ - etc.
94
+ """
95
+
96
+
97
+ class JsonPatchTestFailed(JsonPatchException, AssertionError):
98
+ """ A Test operation failed """
99
+
100
+
101
+ def multidict(ordered_pairs):
102
+ """Convert duplicate keys values to lists."""
103
+ # read all values into lists
104
+ mdict = collections.defaultdict(list)
105
+ for key, value in ordered_pairs:
106
+ mdict[key].append(value)
107
+
108
+ return dict(
109
+ # unpack lists that have only 1 item
110
+ (key, values[0] if len(values) == 1 else values)
111
+ for key, values in mdict.items()
112
+ )
113
+
114
+
115
+ # The "object_pairs_hook" parameter is used to handle duplicate keys when
116
+ # loading a JSON object.
117
+ _jsonloads = functools.partial(json.loads, object_pairs_hook=multidict)
118
+
119
+
120
+ def apply_patch(doc, patch, in_place=False, pointer_cls=JsonPointer):
121
+ """Apply list of patches to specified json document.
122
+
123
+ :param doc: Document object.
124
+ :type doc: dict
125
+
126
+ :param patch: JSON patch as list of dicts or raw JSON-encoded string.
127
+ :type patch: list or str
128
+
129
+ :param in_place: While :const:`True` patch will modify target document.
130
+ By default patch will be applied to document copy.
131
+ :type in_place: bool
132
+
133
+ :param pointer_cls: JSON pointer class to use.
134
+ :type pointer_cls: Type[JsonPointer]
135
+
136
+ :return: Patched document object.
137
+ :rtype: dict
138
+
139
+ >>> doc = {'foo': 'bar'}
140
+ >>> patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}]
141
+ >>> other = apply_patch(doc, patch)
142
+ >>> doc is not other
143
+ True
144
+ >>> other == {'foo': 'bar', 'baz': 'qux'}
145
+ True
146
+ >>> patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}]
147
+ >>> apply_patch(doc, patch, in_place=True) == {'foo': 'bar', 'baz': 'qux'}
148
+ True
149
+ >>> doc == other
150
+ True
151
+ """
152
+
153
+ if isinstance(patch, basestring):
154
+ patch = JsonPatch.from_string(patch, pointer_cls=pointer_cls)
155
+ else:
156
+ patch = JsonPatch(patch, pointer_cls=pointer_cls)
157
+ return patch.apply(doc, in_place)
158
+
159
+
160
+ def make_patch(src, dst, pointer_cls=JsonPointer):
161
+ """Generates patch by comparing two document objects. Actually is
162
+ a proxy to :meth:`JsonPatch.from_diff` method.
163
+
164
+ :param src: Data source document object.
165
+ :type src: dict
166
+
167
+ :param dst: Data source document object.
168
+ :type dst: dict
169
+
170
+ :param pointer_cls: JSON pointer class to use.
171
+ :type pointer_cls: Type[JsonPointer]
172
+
173
+ >>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]}
174
+ >>> dst = {'baz': 'qux', 'numbers': [1, 4, 7]}
175
+ >>> patch = make_patch(src, dst)
176
+ >>> new = patch.apply(src)
177
+ >>> new == dst
178
+ True
179
+ """
180
+
181
+ return JsonPatch.from_diff(src, dst, pointer_cls=pointer_cls)
182
+
183
+
184
+ class PatchOperation(object):
185
+ """A single operation inside a JSON Patch."""
186
+
187
+ def __init__(self, operation, pointer_cls=JsonPointer):
188
+ self.pointer_cls = pointer_cls
189
+
190
+ if not operation.__contains__('path'):
191
+ raise InvalidJsonPatch("Operation must have a 'path' member")
192
+
193
+ if isinstance(operation['path'], self.pointer_cls):
194
+ self.location = operation['path'].path
195
+ self.pointer = operation['path']
196
+ else:
197
+ self.location = operation['path']
198
+ try:
199
+ self.pointer = self.pointer_cls(self.location)
200
+ except TypeError as ex:
201
+ raise InvalidJsonPatch("Invalid 'path'")
202
+
203
+ self.operation = operation
204
+
205
+ def apply(self, obj):
206
+ """Abstract method that applies a patch operation to the specified object."""
207
+ raise NotImplementedError('should implement the patch operation.')
208
+
209
+ def __hash__(self):
210
+ return hash(frozenset(self.operation.items()))
211
+
212
+ def __eq__(self, other):
213
+ if not isinstance(other, PatchOperation):
214
+ return False
215
+ return self.operation == other.operation
216
+
217
+ def __ne__(self, other):
218
+ return not(self == other)
219
+
220
+ @property
221
+ def path(self):
222
+ return '/'.join(self.pointer.parts[:-1])
223
+
224
+ @property
225
+ def key(self):
226
+ try:
227
+ return int(self.pointer.parts[-1])
228
+ except ValueError:
229
+ return self.pointer.parts[-1]
230
+
231
+ @key.setter
232
+ def key(self, value):
233
+ self.pointer.parts[-1] = str(value)
234
+ self.location = self.pointer.path
235
+ self.operation['path'] = self.location
236
+
237
+
238
+ class RemoveOperation(PatchOperation):
239
+ """Removes an object property or an array element."""
240
+
241
+ def apply(self, obj):
242
+ subobj, part = self.pointer.to_last(obj)
243
+
244
+ if isinstance(subobj, Sequence) and not isinstance(part, int):
245
+ raise JsonPointerException("invalid array index '{0}'".format(part))
246
+
247
+ try:
248
+ del subobj[part]
249
+ except (KeyError, IndexError) as ex:
250
+ msg = "can't remove a non-existent object '{0}'".format(part)
251
+ raise JsonPatchConflict(msg)
252
+
253
+ return obj
254
+
255
+ def _on_undo_remove(self, path, key):
256
+ if self.path == path:
257
+ if self.key >= key:
258
+ self.key += 1
259
+ else:
260
+ key -= 1
261
+ return key
262
+
263
+ def _on_undo_add(self, path, key):
264
+ if self.path == path:
265
+ if self.key > key:
266
+ self.key -= 1
267
+ else:
268
+ key -= 1
269
+ return key
270
+
271
+
272
+ class AddOperation(PatchOperation):
273
+ """Adds an object property or an array element."""
274
+
275
+ def apply(self, obj):
276
+ try:
277
+ value = self.operation["value"]
278
+ except KeyError as ex:
279
+ raise InvalidJsonPatch(
280
+ "The operation does not contain a 'value' member")
281
+
282
+ subobj, part = self.pointer.to_last(obj)
283
+
284
+ if isinstance(subobj, MutableSequence):
285
+ if part == '-':
286
+ subobj.append(value) # pylint: disable=E1103
287
+
288
+ elif part > len(subobj) or part < 0:
289
+ raise JsonPatchConflict("can't insert outside of list")
290
+
291
+ else:
292
+ subobj.insert(part, value) # pylint: disable=E1103
293
+
294
+ elif isinstance(subobj, MutableMapping):
295
+ if part is None:
296
+ obj = value # we're replacing the root
297
+ else:
298
+ subobj[part] = value
299
+
300
+ else:
301
+ if part is None:
302
+ raise TypeError("invalid document type {0}".format(type(subobj)))
303
+ else:
304
+ raise JsonPatchConflict("unable to fully resolve json pointer {0}, part {1}".format(self.location, part))
305
+ return obj
306
+
307
+ def _on_undo_remove(self, path, key):
308
+ if self.path == path:
309
+ if self.key > key:
310
+ self.key += 1
311
+ else:
312
+ key += 1
313
+ return key
314
+
315
+ def _on_undo_add(self, path, key):
316
+ if self.path == path:
317
+ if self.key > key:
318
+ self.key -= 1
319
+ else:
320
+ key += 1
321
+ return key
322
+
323
+
324
+ class ReplaceOperation(PatchOperation):
325
+ """Replaces an object property or an array element by a new value."""
326
+
327
+ def apply(self, obj):
328
+ try:
329
+ value = self.operation["value"]
330
+ except KeyError as ex:
331
+ raise InvalidJsonPatch(
332
+ "The operation does not contain a 'value' member")
333
+
334
+ subobj, part = self.pointer.to_last(obj)
335
+
336
+ if part is None:
337
+ return value
338
+
339
+ if part == "-":
340
+ raise InvalidJsonPatch("'path' with '-' can't be applied to 'replace' operation")
341
+
342
+ if isinstance(subobj, MutableSequence):
343
+ if part >= len(subobj) or part < 0:
344
+ raise JsonPatchConflict("can't replace outside of list")
345
+
346
+ elif isinstance(subobj, MutableMapping):
347
+ if part not in subobj:
348
+ msg = "can't replace a non-existent object '{0}'".format(part)
349
+ raise JsonPatchConflict(msg)
350
+ else:
351
+ if part is None:
352
+ raise TypeError("invalid document type {0}".format(type(subobj)))
353
+ else:
354
+ raise JsonPatchConflict("unable to fully resolve json pointer {0}, part {1}".format(self.location, part))
355
+
356
+ subobj[part] = value
357
+ return obj
358
+
359
+ def _on_undo_remove(self, path, key):
360
+ return key
361
+
362
+ def _on_undo_add(self, path, key):
363
+ return key
364
+
365
+
366
+ class MoveOperation(PatchOperation):
367
+ """Moves an object property or an array element to a new location."""
368
+
369
+ def apply(self, obj):
370
+ try:
371
+ if isinstance(self.operation['from'], self.pointer_cls):
372
+ from_ptr = self.operation['from']
373
+ else:
374
+ from_ptr = self.pointer_cls(self.operation['from'])
375
+ except KeyError as ex:
376
+ raise InvalidJsonPatch(
377
+ "The operation does not contain a 'from' member")
378
+
379
+ subobj, part = from_ptr.to_last(obj)
380
+ try:
381
+ value = subobj[part]
382
+ except (KeyError, IndexError) as ex:
383
+ raise JsonPatchConflict(str(ex))
384
+
385
+ # If source and target are equal, this is a no-op
386
+ if self.pointer == from_ptr:
387
+ return obj
388
+
389
+ if isinstance(subobj, MutableMapping) and \
390
+ self.pointer.contains(from_ptr):
391
+ raise JsonPatchConflict('Cannot move values into their own children')
392
+
393
+ obj = RemoveOperation({
394
+ 'op': 'remove',
395
+ 'path': self.operation['from']
396
+ }, pointer_cls=self.pointer_cls).apply(obj)
397
+
398
+ obj = AddOperation({
399
+ 'op': 'add',
400
+ 'path': self.location,
401
+ 'value': value
402
+ }, pointer_cls=self.pointer_cls).apply(obj)
403
+
404
+ return obj
405
+
406
+ @property
407
+ def from_path(self):
408
+ from_ptr = self.pointer_cls(self.operation['from'])
409
+ return '/'.join(from_ptr.parts[:-1])
410
+
411
+ @property
412
+ def from_key(self):
413
+ from_ptr = self.pointer_cls(self.operation['from'])
414
+ try:
415
+ return int(from_ptr.parts[-1])
416
+ except TypeError:
417
+ return from_ptr.parts[-1]
418
+
419
+ @from_key.setter
420
+ def from_key(self, value):
421
+ from_ptr = self.pointer_cls(self.operation['from'])
422
+ from_ptr.parts[-1] = str(value)
423
+ self.operation['from'] = from_ptr.path
424
+
425
+ def _on_undo_remove(self, path, key):
426
+ if self.from_path == path:
427
+ if self.from_key >= key:
428
+ self.from_key += 1
429
+ else:
430
+ key -= 1
431
+ if self.path == path:
432
+ if self.key > key:
433
+ self.key += 1
434
+ else:
435
+ key += 1
436
+ return key
437
+
438
+ def _on_undo_add(self, path, key):
439
+ if self.from_path == path:
440
+ if self.from_key > key:
441
+ self.from_key -= 1
442
+ else:
443
+ key -= 1
444
+ if self.path == path:
445
+ if self.key > key:
446
+ self.key -= 1
447
+ else:
448
+ key += 1
449
+ return key
450
+
451
+
452
+ class TestOperation(PatchOperation):
453
+ """Test value by specified location."""
454
+
455
+ def apply(self, obj):
456
+ try:
457
+ subobj, part = self.pointer.to_last(obj)
458
+ if part is None:
459
+ val = subobj
460
+ else:
461
+ val = self.pointer.walk(subobj, part)
462
+ except JsonPointerException as ex:
463
+ raise JsonPatchTestFailed(str(ex))
464
+
465
+ try:
466
+ value = self.operation['value']
467
+ except KeyError as ex:
468
+ raise InvalidJsonPatch(
469
+ "The operation does not contain a 'value' member")
470
+
471
+ if val != value:
472
+ msg = '{0} ({1}) is not equal to tested value {2} ({3})'
473
+ raise JsonPatchTestFailed(msg.format(val, type(val),
474
+ value, type(value)))
475
+
476
+ return obj
477
+
478
+
479
+ class CopyOperation(PatchOperation):
480
+ """ Copies an object property or an array element to a new location """
481
+
482
+ def apply(self, obj):
483
+ try:
484
+ from_ptr = self.pointer_cls(self.operation['from'])
485
+ except KeyError as ex:
486
+ raise InvalidJsonPatch(
487
+ "The operation does not contain a 'from' member")
488
+
489
+ subobj, part = from_ptr.to_last(obj)
490
+ try:
491
+ value = copy.deepcopy(subobj[part])
492
+ except (KeyError, IndexError) as ex:
493
+ raise JsonPatchConflict(str(ex))
494
+
495
+ obj = AddOperation({
496
+ 'op': 'add',
497
+ 'path': self.location,
498
+ 'value': value
499
+ }, pointer_cls=self.pointer_cls).apply(obj)
500
+
501
+ return obj
502
+
503
+
504
+ class JsonPatch(object):
505
+ json_dumper = staticmethod(json.dumps)
506
+ json_loader = staticmethod(_jsonloads)
507
+
508
+ operations = MappingProxyType({
509
+ 'remove': RemoveOperation,
510
+ 'add': AddOperation,
511
+ 'replace': ReplaceOperation,
512
+ 'move': MoveOperation,
513
+ 'test': TestOperation,
514
+ 'copy': CopyOperation,
515
+ })
516
+
517
+ """A JSON Patch is a list of Patch Operations.
518
+
519
+ >>> patch = JsonPatch([
520
+ ... {'op': 'add', 'path': '/foo', 'value': 'bar'},
521
+ ... {'op': 'add', 'path': '/baz', 'value': [1, 2, 3]},
522
+ ... {'op': 'remove', 'path': '/baz/1'},
523
+ ... {'op': 'test', 'path': '/baz', 'value': [1, 3]},
524
+ ... {'op': 'replace', 'path': '/baz/0', 'value': 42},
525
+ ... {'op': 'remove', 'path': '/baz/1'},
526
+ ... ])
527
+ >>> doc = {}
528
+ >>> result = patch.apply(doc)
529
+ >>> expected = {'foo': 'bar', 'baz': [42]}
530
+ >>> result == expected
531
+ True
532
+
533
+ JsonPatch object is iterable, so you can easily access each patch
534
+ statement in a loop:
535
+
536
+ >>> lpatch = list(patch)
537
+ >>> expected = {'op': 'add', 'path': '/foo', 'value': 'bar'}
538
+ >>> lpatch[0] == expected
539
+ True
540
+ >>> lpatch == patch.patch
541
+ True
542
+
543
+ Also JsonPatch could be converted directly to :class:`bool` if it contains
544
+ any operation statements:
545
+
546
+ >>> bool(patch)
547
+ True
548
+ >>> bool(JsonPatch([]))
549
+ False
550
+
551
+ This behavior is very handy with :func:`make_patch` to write more readable
552
+ code:
553
+
554
+ >>> old = {'foo': 'bar', 'numbers': [1, 3, 4, 8]}
555
+ >>> new = {'baz': 'qux', 'numbers': [1, 4, 7]}
556
+ >>> patch = make_patch(old, new)
557
+ >>> if patch:
558
+ ... # document have changed, do something useful
559
+ ... patch.apply(old) #doctest: +ELLIPSIS
560
+ {...}
561
+ """
562
+ def __init__(self, patch, pointer_cls=JsonPointer):
563
+ self.patch = patch
564
+ self.pointer_cls = pointer_cls
565
+
566
+ # Verify that the structure of the patch document
567
+ # is correct by retrieving each patch element.
568
+ # Much of the validation is done in the initializer
569
+ # though some is delayed until the patch is applied.
570
+ for op in self.patch:
571
+ # We're only checking for basestring in the following check
572
+ # for two reasons:
573
+ #
574
+ # - It should come from JSON, which only allows strings as
575
+ # dictionary keys, so having a string here unambiguously means
576
+ # someone used: {"op": ..., ...} instead of [{"op": ..., ...}].
577
+ #
578
+ # - There's no possible false positive: if someone give a sequence
579
+ # of mappings, this won't raise.
580
+ if isinstance(op, basestring):
581
+ raise InvalidJsonPatch("Document is expected to be sequence of "
582
+ "operations, got a sequence of strings.")
583
+
584
+ self._get_operation(op)
585
+
586
+ def __str__(self):
587
+ """str(self) -> self.to_string()"""
588
+ return self.to_string()
589
+
590
+ def __bool__(self):
591
+ return bool(self.patch)
592
+
593
+ __nonzero__ = __bool__
594
+
595
+ def __iter__(self):
596
+ return iter(self.patch)
597
+
598
+ def __hash__(self):
599
+ return hash(tuple(self._ops))
600
+
601
+ def __eq__(self, other):
602
+ if not isinstance(other, JsonPatch):
603
+ return False
604
+ return self._ops == other._ops
605
+
606
+ def __ne__(self, other):
607
+ return not(self == other)
608
+
609
+ @classmethod
610
+ def from_string(cls, patch_str, loads=None, pointer_cls=JsonPointer):
611
+ """Creates JsonPatch instance from string source.
612
+
613
+ :param patch_str: JSON patch as raw string.
614
+ :type patch_str: str
615
+
616
+ :param loads: A function of one argument that loads a serialized
617
+ JSON string.
618
+ :type loads: function
619
+
620
+ :param pointer_cls: JSON pointer class to use.
621
+ :type pointer_cls: Type[JsonPointer]
622
+
623
+ :return: :class:`JsonPatch` instance.
624
+ """
625
+ json_loader = loads or cls.json_loader
626
+ patch = json_loader(patch_str)
627
+ return cls(patch, pointer_cls=pointer_cls)
628
+
629
+ @classmethod
630
+ def from_diff(
631
+ cls, src, dst, optimization=True, dumps=None,
632
+ pointer_cls=JsonPointer,
633
+ ):
634
+ """Creates JsonPatch instance based on comparison of two document
635
+ objects. Json patch would be created for `src` argument against `dst`
636
+ one.
637
+
638
+ :param src: Data source document object.
639
+ :type src: dict
640
+
641
+ :param dst: Data source document object.
642
+ :type dst: dict
643
+
644
+ :param dumps: A function of one argument that produces a serialized
645
+ JSON string.
646
+ :type dumps: function
647
+
648
+ :param pointer_cls: JSON pointer class to use.
649
+ :type pointer_cls: Type[JsonPointer]
650
+
651
+ :return: :class:`JsonPatch` instance.
652
+
653
+ >>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]}
654
+ >>> dst = {'baz': 'qux', 'numbers': [1, 4, 7]}
655
+ >>> patch = JsonPatch.from_diff(src, dst)
656
+ >>> new = patch.apply(src)
657
+ >>> new == dst
658
+ True
659
+ """
660
+ json_dumper = dumps or cls.json_dumper
661
+ builder = DiffBuilder(src, dst, json_dumper, pointer_cls=pointer_cls)
662
+ builder._compare_values('', None, src, dst)
663
+ ops = list(builder.execute())
664
+ return cls(ops, pointer_cls=pointer_cls)
665
+
666
+ def to_string(self, dumps=None):
667
+ """Returns patch set as JSON string."""
668
+ json_dumper = dumps or self.json_dumper
669
+ return json_dumper(self.patch)
670
+
671
+ @property
672
+ def _ops(self):
673
+ return tuple(map(self._get_operation, self.patch))
674
+
675
+ def apply(self, obj, in_place=False):
676
+ """Applies the patch to a given object.
677
+
678
+ :param obj: Document object.
679
+ :type obj: dict
680
+
681
+ :param in_place: Tweaks the way how patch would be applied - directly to
682
+ specified `obj` or to its copy.
683
+ :type in_place: bool
684
+
685
+ :return: Modified `obj`.
686
+ """
687
+
688
+ if not in_place:
689
+ obj = copy.deepcopy(obj)
690
+
691
+ for operation in self._ops:
692
+ obj = operation.apply(obj)
693
+
694
+ return obj
695
+
696
+ def _get_operation(self, operation):
697
+ if 'op' not in operation:
698
+ raise InvalidJsonPatch("Operation does not contain 'op' member")
699
+
700
+ op = operation['op']
701
+
702
+ if not isinstance(op, basestring):
703
+ raise InvalidJsonPatch("Operation's op must be a string")
704
+
705
+ if op not in self.operations:
706
+ raise InvalidJsonPatch("Unknown operation {0!r}".format(op))
707
+
708
+ cls = self.operations[op]
709
+ return cls(operation, pointer_cls=self.pointer_cls)
710
+
711
+
712
+ class DiffBuilder(object):
713
+
714
+ def __init__(self, src_doc, dst_doc, dumps=json.dumps, pointer_cls=JsonPointer):
715
+ self.dumps = dumps
716
+ self.pointer_cls = pointer_cls
717
+ self.index_storage = [{}, {}]
718
+ self.index_storage2 = [[], []]
719
+ self.__root = root = []
720
+ self.src_doc = src_doc
721
+ self.dst_doc = dst_doc
722
+ root[:] = [root, root, None]
723
+
724
+ def store_index(self, value, index, st):
725
+ typed_key = (value, type(value))
726
+ try:
727
+ storage = self.index_storage[st]
728
+ stored = storage.get(typed_key)
729
+ if stored is None:
730
+ storage[typed_key] = [index]
731
+ else:
732
+ storage[typed_key].append(index)
733
+
734
+ except TypeError:
735
+ self.index_storage2[st].append((typed_key, index))
736
+
737
+ def take_index(self, value, st):
738
+ typed_key = (value, type(value))
739
+ try:
740
+ stored = self.index_storage[st].get(typed_key)
741
+ if stored:
742
+ return stored.pop()
743
+
744
+ except TypeError:
745
+ storage = self.index_storage2[st]
746
+ for i in range(len(storage)-1, -1, -1):
747
+ if storage[i][0] == typed_key:
748
+ return storage.pop(i)[1]
749
+
750
+ def insert(self, op):
751
+ root = self.__root
752
+ last = root[0]
753
+ last[1] = root[0] = [last, root, op]
754
+ return root[0]
755
+
756
+ def remove(self, index):
757
+ link_prev, link_next, _ = index
758
+ link_prev[1] = link_next
759
+ link_next[0] = link_prev
760
+ index[:] = []
761
+
762
+ def iter_from(self, start):
763
+ root = self.__root
764
+ curr = start[1]
765
+ while curr is not root:
766
+ yield curr[2]
767
+ curr = curr[1]
768
+
769
+ def __iter__(self):
770
+ root = self.__root
771
+ curr = root[1]
772
+ while curr is not root:
773
+ yield curr[2]
774
+ curr = curr[1]
775
+
776
+ def execute(self):
777
+ root = self.__root
778
+ curr = root[1]
779
+ while curr is not root:
780
+ if curr[1] is not root:
781
+ op_first, op_second = curr[2], curr[1][2]
782
+ if op_first.location == op_second.location and \
783
+ type(op_first) == RemoveOperation and \
784
+ type(op_second) == AddOperation:
785
+ yield ReplaceOperation({
786
+ 'op': 'replace',
787
+ 'path': op_second.location,
788
+ 'value': op_second.operation['value'],
789
+ }, pointer_cls=self.pointer_cls).operation
790
+ curr = curr[1][1]
791
+ continue
792
+
793
+ yield curr[2].operation
794
+ curr = curr[1]
795
+
796
+ def _item_added(self, path, key, item):
797
+ index = self.take_index(item, _ST_REMOVE)
798
+ if index is not None:
799
+ op = index[2]
800
+ if type(op.key) == int and type(key) == int:
801
+ for v in self.iter_from(index):
802
+ op.key = v._on_undo_remove(op.path, op.key)
803
+
804
+ self.remove(index)
805
+ if op.location != _path_join(path, key):
806
+ new_op = MoveOperation({
807
+ 'op': 'move',
808
+ 'from': op.location,
809
+ 'path': _path_join(path, key),
810
+ }, pointer_cls=self.pointer_cls)
811
+ self.insert(new_op)
812
+ else:
813
+ new_op = AddOperation({
814
+ 'op': 'add',
815
+ 'path': _path_join(path, key),
816
+ 'value': item,
817
+ }, pointer_cls=self.pointer_cls)
818
+ new_index = self.insert(new_op)
819
+ self.store_index(item, new_index, _ST_ADD)
820
+
821
+ def _item_removed(self, path, key, item):
822
+ new_op = RemoveOperation({
823
+ 'op': 'remove',
824
+ 'path': _path_join(path, key),
825
+ }, pointer_cls=self.pointer_cls)
826
+ index = self.take_index(item, _ST_ADD)
827
+ new_index = self.insert(new_op)
828
+ if index is not None:
829
+ op = index[2]
830
+ # We can't rely on the op.key type since PatchOperation casts
831
+ # the .key property to int and this path wrongly ends up being taken
832
+ # for numeric string dict keys while the intention is to only handle lists.
833
+ # So we do an explicit check on the item affected by the op instead.
834
+ added_item = op.pointer.to_last(self.dst_doc)[0]
835
+ if type(added_item) == list:
836
+ for v in self.iter_from(index):
837
+ op.key = v._on_undo_add(op.path, op.key)
838
+
839
+ self.remove(index)
840
+ if new_op.location != op.location:
841
+ new_op = MoveOperation({
842
+ 'op': 'move',
843
+ 'from': new_op.location,
844
+ 'path': op.location,
845
+ }, pointer_cls=self.pointer_cls)
846
+ new_index[2] = new_op
847
+
848
+ else:
849
+ self.remove(new_index)
850
+
851
+ else:
852
+ self.store_index(item, new_index, _ST_REMOVE)
853
+
854
+ def _item_replaced(self, path, key, item):
855
+ self.insert(ReplaceOperation({
856
+ 'op': 'replace',
857
+ 'path': _path_join(path, key),
858
+ 'value': item,
859
+ }, pointer_cls=self.pointer_cls))
860
+
861
+ def _compare_dicts(self, path, src, dst):
862
+ src_keys = set(src.keys())
863
+ dst_keys = set(dst.keys())
864
+ added_keys = dst_keys - src_keys
865
+ removed_keys = src_keys - dst_keys
866
+
867
+ for key in removed_keys:
868
+ self._item_removed(path, str(key), src[key])
869
+
870
+ for key in added_keys:
871
+ self._item_added(path, str(key), dst[key])
872
+
873
+ for key in src_keys & dst_keys:
874
+ self._compare_values(path, key, src[key], dst[key])
875
+
876
+ def _compare_lists(self, path, src, dst):
877
+ len_src, len_dst = len(src), len(dst)
878
+ max_len = max(len_src, len_dst)
879
+ min_len = min(len_src, len_dst)
880
+ for key in range(max_len):
881
+ if key < min_len:
882
+ old, new = src[key], dst[key]
883
+ if old == new:
884
+ continue
885
+
886
+ elif isinstance(old, MutableMapping) and \
887
+ isinstance(new, MutableMapping):
888
+ self._compare_dicts(_path_join(path, key), old, new)
889
+
890
+ elif isinstance(old, MutableSequence) and \
891
+ isinstance(new, MutableSequence):
892
+ self._compare_lists(_path_join(path, key), old, new)
893
+
894
+ else:
895
+ self._item_removed(path, key, old)
896
+ self._item_added(path, key, new)
897
+
898
+ elif len_src > len_dst:
899
+ self._item_removed(path, len_dst, src[key])
900
+
901
+ else:
902
+ self._item_added(path, key, dst[key])
903
+
904
+ def _compare_values(self, path, key, src, dst):
905
+ if isinstance(src, MutableMapping) and \
906
+ isinstance(dst, MutableMapping):
907
+ self._compare_dicts(_path_join(path, key), src, dst)
908
+
909
+ elif isinstance(src, MutableSequence) and \
910
+ isinstance(dst, MutableSequence):
911
+ self._compare_lists(_path_join(path, key), src, dst)
912
+
913
+ # To ensure we catch changes to JSON, we can't rely on a simple
914
+ # src == dst, because it would not recognize the difference between
915
+ # 1 and True, among other things. Using json.dumps is the most
916
+ # fool-proof way to ensure we catch type changes that matter to JSON
917
+ # and ignore those that don't. The performance of this could be
918
+ # improved by doing more direct type checks, but we'd need to be
919
+ # careful to accept type changes that don't matter when JSONified.
920
+ elif self.dumps(src) == self.dumps(dst):
921
+ return
922
+
923
+ else:
924
+ self._item_replaced(path, key, dst)
925
+
926
+
927
+ def _path_join(path, key):
928
+ if key is None:
929
+ return path
930
+
931
+ return path + '/' + str(key).replace('~', '~0').replace('/', '~1')