fastjsonpatch 0.1.0__cp312-abi3-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1020 @@
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) - a faster drop-in for jsonpatch 1.34.
34
+
35
+ This is jsonpatch's module with its hot paths moved to Rust (``fastjsonpatch._core``):
36
+
37
+ - ``JsonPatch.from_diff`` / ``make_patch``: ``_core.diff`` yields the same operations
38
+ as ``DiffBuilder``; it returns None for inputs it cannot reproduce exactly, and
39
+ ``DiffBuilder`` (kept below, unchanged) runs instead.
40
+ - ``JsonPatch.apply`` / ``apply_patch``: ``_core.apply`` applies operations to plain
41
+ dicts and lists. An operation it cannot apply exactly goes to the operation
42
+ classes below before anything is mutated.
43
+ - ``JsonPatch.__init__``: ``_core.validate`` checks operations; unusual ones are
44
+ checked by ``_get_operation`` as before.
45
+
46
+ Custom pointer classes, custom ``operations`` and subclasses that override
47
+ ``_get_operation`` always use the pure-Python code.
48
+ """
49
+
50
+ import collections
51
+ import copy
52
+ import functools
53
+ import importlib.metadata
54
+ import json
55
+ from collections.abc import MutableMapping, MutableSequence, Sequence
56
+ from types import MappingProxyType
57
+
58
+ from jsonpointer import JsonPointer, JsonPointerException
59
+
60
+ from . import _core
61
+
62
+
63
+ _ST_ADD = 0
64
+ _ST_REMOVE = 1
65
+
66
+
67
+ __version__ = importlib.metadata.version('fastjsonpatch')
68
+ __license__ = 'BSD-3-Clause'
69
+ # Version of jsonpatch whose behaviour this module reproduces.
70
+ __jsonpatch_version__ = '1.34'
71
+
72
+
73
+ class JsonPatchException(Exception):
74
+ """Base Json Patch exception"""
75
+
76
+
77
+ class InvalidJsonPatch(JsonPatchException):
78
+ """ Raised if an invalid JSON Patch is created """
79
+
80
+
81
+ class JsonPatchConflict(JsonPatchException):
82
+ """Raised if patch could not be applied due to conflict situation such as:
83
+ - attempt to add object key when it already exists;
84
+ - attempt to operate with nonexistence object key;
85
+ - attempt to insert value to array at position beyond its size;
86
+ - etc.
87
+ """
88
+
89
+
90
+ class JsonPatchTestFailed(JsonPatchException, AssertionError):
91
+ """ A Test operation failed """
92
+
93
+
94
+ def multidict(ordered_pairs):
95
+ """Convert duplicate keys values to lists."""
96
+ # read all values into lists
97
+ mdict = collections.defaultdict(list)
98
+ for key, value in ordered_pairs:
99
+ mdict[key].append(value)
100
+
101
+ return dict(
102
+ # unpack lists that have only 1 item
103
+ (key, values[0] if len(values) == 1 else values)
104
+ for key, values in mdict.items()
105
+ )
106
+
107
+
108
+ # The "object_pairs_hook" parameter is used to handle duplicate keys when
109
+ # loading a JSON object.
110
+ _jsonloads = functools.partial(json.loads, object_pairs_hook=multidict)
111
+
112
+
113
+ def apply_patch(doc, patch, in_place=False, pointer_cls=JsonPointer):
114
+ """Apply list of patches to specified json document.
115
+
116
+ :param doc: Document object.
117
+ :type doc: dict
118
+
119
+ :param patch: JSON patch as list of dicts or raw JSON-encoded string.
120
+ :type patch: list or str
121
+
122
+ :param in_place: While :const:`True` patch will modify target document.
123
+ By default patch will be applied to document copy.
124
+ :type in_place: bool
125
+
126
+ :param pointer_cls: JSON pointer class to use.
127
+ :type pointer_cls: Type[JsonPointer]
128
+
129
+ :return: Patched document object.
130
+ :rtype: dict
131
+
132
+ >>> doc = {'foo': 'bar'}
133
+ >>> patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}]
134
+ >>> other = apply_patch(doc, patch)
135
+ >>> doc is not other
136
+ True
137
+ >>> other == {'foo': 'bar', 'baz': 'qux'}
138
+ True
139
+ >>> patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}]
140
+ >>> apply_patch(doc, patch, in_place=True) == {'foo': 'bar', 'baz': 'qux'}
141
+ True
142
+ >>> doc == other
143
+ True
144
+ """
145
+
146
+ if isinstance(patch, (str, bytes)):
147
+ patch = JsonPatch.from_string(patch, pointer_cls=pointer_cls)
148
+ else:
149
+ patch = JsonPatch(patch, pointer_cls=pointer_cls)
150
+ return patch.apply(doc, in_place)
151
+
152
+
153
+ def make_patch(src, dst, pointer_cls=JsonPointer):
154
+ """Generates patch by comparing two document objects. Actually is
155
+ a proxy to :meth:`JsonPatch.from_diff` method. The resulting patch
156
+ transforms `src` into `dst`, so it has to be applied to `src` (or to a
157
+ document equal to it).
158
+
159
+ :param src: Data source document object.
160
+ :type src: dict
161
+
162
+ :param dst: Data target document object.
163
+ :type dst: dict
164
+
165
+ :param pointer_cls: JSON pointer class to use.
166
+ :type pointer_cls: Type[JsonPointer]
167
+
168
+ >>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]}
169
+ >>> dst = {'baz': 'qux', 'numbers': [1, 4, 7]}
170
+ >>> patch = make_patch(src, dst)
171
+ >>> new = patch.apply(src)
172
+ >>> new == dst
173
+ True
174
+ """
175
+
176
+ return JsonPatch.from_diff(src, dst, pointer_cls=pointer_cls)
177
+
178
+
179
+ class PatchOperation(object):
180
+ """A single operation inside a JSON Patch."""
181
+
182
+ def __init__(self, operation, pointer_cls=JsonPointer):
183
+ self.pointer_cls = pointer_cls
184
+
185
+ if not operation.__contains__('path'):
186
+ raise InvalidJsonPatch("Operation must have a 'path' member")
187
+
188
+ if isinstance(operation['path'], self.pointer_cls):
189
+ self.location = operation['path'].path
190
+ self.pointer = operation['path']
191
+ else:
192
+ self.location = operation['path']
193
+ try:
194
+ self.pointer = self.pointer_cls(self.location)
195
+ except TypeError:
196
+ raise InvalidJsonPatch("Invalid 'path'")
197
+
198
+ self.operation = operation
199
+
200
+ def apply(self, obj):
201
+ """Abstract method that applies a patch operation to the specified object."""
202
+ raise NotImplementedError('should implement the patch operation.')
203
+
204
+ def __hash__(self):
205
+ return hash(frozenset(self.operation.items()))
206
+
207
+ def __eq__(self, other):
208
+ if not isinstance(other, PatchOperation):
209
+ return False
210
+ return self.operation == other.operation
211
+
212
+ def __ne__(self, other):
213
+ return not(self == other)
214
+
215
+ @property
216
+ def path(self):
217
+ return '/'.join(self.pointer.parts[:-1])
218
+
219
+ @property
220
+ def key(self):
221
+ return self.get_part(-1)
222
+
223
+ @key.setter
224
+ def key(self, value):
225
+ self.set_part(-1, value)
226
+
227
+ def get_part(self, index):
228
+ try:
229
+ return int(self.pointer.parts[index])
230
+ except ValueError:
231
+ return self.pointer.parts[index]
232
+
233
+ def set_part(self, index, value):
234
+ self.pointer.parts[index] = str(value)
235
+ self.location = self.pointer.path
236
+ self.operation['path'] = self.location
237
+
238
+
239
+ class RemoveOperation(PatchOperation):
240
+ """Removes an object property or an array element."""
241
+
242
+ def apply(self, obj):
243
+ subobj, part = _to_last(self.pointer, obj)
244
+
245
+ if isinstance(subobj, Sequence) and not isinstance(part, int):
246
+ raise JsonPointerException("invalid array index '{0}'".format(part))
247
+
248
+ try:
249
+ del subobj[part]
250
+ except (KeyError, IndexError):
251
+ msg = "can't remove a non-existent object '{0}'".format(part)
252
+ raise JsonPatchConflict(msg)
253
+
254
+ return obj
255
+
256
+
257
+ class AddOperation(PatchOperation):
258
+ """Adds an object property or an array element."""
259
+
260
+ def apply(self, obj):
261
+ try:
262
+ value = self.operation["value"]
263
+ except KeyError:
264
+ raise InvalidJsonPatch(
265
+ "The operation does not contain a 'value' member")
266
+
267
+ # Insert a copy so the document does not share mutable values with
268
+ # the patch; otherwise later operations would modify the patch itself.
269
+ return self._add(obj, copy.deepcopy(value))
270
+
271
+ def _add(self, obj, value):
272
+ subobj, part = _to_last(self.pointer, obj)
273
+
274
+ if isinstance(subobj, MutableSequence):
275
+ if part is None:
276
+ return value # we're replacing the root
277
+
278
+ elif part == '-':
279
+ subobj.append(value) # pylint: disable=E1103
280
+
281
+ elif part > len(subobj) or part < 0:
282
+ raise JsonPatchConflict("can't insert outside of list")
283
+
284
+ else:
285
+ subobj.insert(part, value) # pylint: disable=E1103
286
+
287
+ elif isinstance(subobj, MutableMapping):
288
+ if part is None:
289
+ obj = value # we're replacing the root
290
+ else:
291
+ subobj[part] = value
292
+
293
+ else:
294
+ if part is None:
295
+ raise TypeError("invalid document type {0}".format(type(subobj)))
296
+ else:
297
+ raise JsonPatchConflict("unable to fully resolve json pointer {0}, part {1}".format(self.location, part))
298
+ return obj
299
+
300
+
301
+ class ReplaceOperation(PatchOperation):
302
+ """Replaces an object property or an array element by a new value."""
303
+
304
+ def apply(self, obj):
305
+ try:
306
+ value = self.operation["value"]
307
+ except KeyError:
308
+ raise InvalidJsonPatch(
309
+ "The operation does not contain a 'value' member")
310
+
311
+ # copied for the same reason as in AddOperation.apply
312
+ value = copy.deepcopy(value)
313
+
314
+ subobj, part = _to_last(self.pointer, obj)
315
+
316
+ if part is None:
317
+ return value
318
+
319
+ if part == "-":
320
+ raise InvalidJsonPatch("'path' with '-' can't be applied to 'replace' operation")
321
+
322
+ if isinstance(subobj, MutableSequence):
323
+ if part >= len(subobj) or part < 0:
324
+ raise JsonPatchConflict("can't replace outside of list")
325
+
326
+ elif isinstance(subobj, MutableMapping):
327
+ if part not in subobj:
328
+ msg = "can't replace a non-existent object '{0}'".format(part)
329
+ raise JsonPatchConflict(msg)
330
+ else:
331
+ if part is None:
332
+ raise TypeError("invalid document type {0}".format(type(subobj)))
333
+ else:
334
+ raise JsonPatchConflict("unable to fully resolve json pointer {0}, part {1}".format(self.location, part))
335
+
336
+ subobj[part] = value
337
+ return obj
338
+
339
+
340
+ class MoveOperation(PatchOperation):
341
+ """Moves an object property or an array element to a new location."""
342
+
343
+ def apply(self, obj):
344
+ try:
345
+ if isinstance(self.operation['from'], self.pointer_cls):
346
+ from_ptr = self.operation['from']
347
+ else:
348
+ from_ptr = self.pointer_cls(self.operation['from'])
349
+ except KeyError:
350
+ raise InvalidJsonPatch(
351
+ "The operation does not contain a 'from' member")
352
+
353
+ subobj, part = _to_last(from_ptr, obj)
354
+ try:
355
+ value = subobj[part]
356
+ except (KeyError, IndexError) as ex:
357
+ raise JsonPatchConflict(str(ex))
358
+
359
+ # If source and target are equal, this is a no-op
360
+ if self.pointer == from_ptr:
361
+ return obj
362
+
363
+ if isinstance(subobj, MutableMapping) and \
364
+ self.pointer.contains(from_ptr):
365
+ raise JsonPatchConflict('Cannot move values into their own children')
366
+
367
+ obj = RemoveOperation({
368
+ 'op': 'remove',
369
+ 'path': self.operation['from']
370
+ }, pointer_cls=self.pointer_cls).apply(obj)
371
+
372
+ # the value has been detached from its old location, so no copy needed
373
+ obj = AddOperation({
374
+ 'op': 'add',
375
+ 'path': self.location,
376
+ }, pointer_cls=self.pointer_cls)._add(obj, value)
377
+
378
+ return obj
379
+
380
+ @property
381
+ def from_path(self):
382
+ from_ptr = self.pointer_cls(self.operation['from'])
383
+ return '/'.join(from_ptr.parts[:-1])
384
+
385
+ @property
386
+ def from_key(self):
387
+ return self.get_from_part(-1)
388
+
389
+ @from_key.setter
390
+ def from_key(self, value):
391
+ self.set_from_part(-1, value)
392
+
393
+ def get_from_part(self, index):
394
+ from_ptr = self.pointer_cls(self.operation['from'])
395
+ try:
396
+ return int(from_ptr.parts[index])
397
+ except ValueError:
398
+ return from_ptr.parts[index]
399
+
400
+ def set_from_part(self, index, value):
401
+ from_ptr = self.pointer_cls(self.operation['from'])
402
+ from_ptr.parts[index] = str(value)
403
+ self.operation['from'] = from_ptr.path
404
+
405
+
406
+ class TestOperation(PatchOperation):
407
+ """Test value by specified location."""
408
+
409
+ def apply(self, obj):
410
+ try:
411
+ subobj, part = _to_last(self.pointer, obj)
412
+ if part is None:
413
+ val = subobj
414
+ else:
415
+ val = self.pointer.walk(subobj, part)
416
+ except JsonPointerException as ex:
417
+ raise JsonPatchTestFailed(str(ex))
418
+
419
+ try:
420
+ value = self.operation['value']
421
+ except KeyError:
422
+ raise InvalidJsonPatch(
423
+ "The operation does not contain a 'value' member")
424
+
425
+ if val != value:
426
+ msg = '{0} ({1}) is not equal to tested value {2} ({3})'
427
+ raise JsonPatchTestFailed(msg.format(val, type(val),
428
+ value, type(value)))
429
+
430
+ return obj
431
+
432
+
433
+ class CopyOperation(PatchOperation):
434
+ """ Copies an object property or an array element to a new location """
435
+
436
+ def apply(self, obj):
437
+ try:
438
+ from_ptr = self.pointer_cls(self.operation['from'])
439
+ except KeyError:
440
+ raise InvalidJsonPatch(
441
+ "The operation does not contain a 'from' member")
442
+
443
+ subobj, part = _to_last(from_ptr, obj)
444
+ try:
445
+ value = copy.deepcopy(subobj if part is None else subobj[part])
446
+ except (KeyError, IndexError) as ex:
447
+ raise JsonPatchConflict(str(ex))
448
+
449
+ # value is already a deep copy, no need to copy it again
450
+ obj = AddOperation({
451
+ 'op': 'add',
452
+ 'path': self.location,
453
+ }, pointer_cls=self.pointer_cls)._add(obj, value)
454
+
455
+ return obj
456
+
457
+
458
+ class JsonPatch(object):
459
+ json_dumper = staticmethod(json.dumps)
460
+ json_loader = staticmethod(_jsonloads)
461
+
462
+ operations = MappingProxyType({
463
+ 'remove': RemoveOperation,
464
+ 'add': AddOperation,
465
+ 'replace': ReplaceOperation,
466
+ 'move': MoveOperation,
467
+ 'test': TestOperation,
468
+ 'copy': CopyOperation,
469
+ })
470
+
471
+ """A JSON Patch is a list of Patch Operations.
472
+
473
+ >>> patch = JsonPatch([
474
+ ... {'op': 'add', 'path': '/foo', 'value': 'bar'},
475
+ ... {'op': 'add', 'path': '/baz', 'value': [1, 2, 3]},
476
+ ... {'op': 'remove', 'path': '/baz/1'},
477
+ ... {'op': 'test', 'path': '/baz', 'value': [1, 3]},
478
+ ... {'op': 'replace', 'path': '/baz/0', 'value': 42},
479
+ ... {'op': 'remove', 'path': '/baz/1'},
480
+ ... ])
481
+ >>> doc = {}
482
+ >>> result = patch.apply(doc)
483
+ >>> expected = {'foo': 'bar', 'baz': [42]}
484
+ >>> result == expected
485
+ True
486
+
487
+ JsonPatch object is iterable, so you can easily access each patch
488
+ statement in a loop:
489
+
490
+ >>> lpatch = list(patch)
491
+ >>> expected = {'op': 'add', 'path': '/foo', 'value': 'bar'}
492
+ >>> lpatch[0] == expected
493
+ True
494
+ >>> lpatch == patch.patch
495
+ True
496
+
497
+ Also JsonPatch could be converted directly to :class:`bool` if it contains
498
+ any operation statements:
499
+
500
+ >>> bool(patch)
501
+ True
502
+ >>> bool(JsonPatch([]))
503
+ False
504
+
505
+ This behavior is very handy with :func:`make_patch` to write more readable
506
+ code:
507
+
508
+ >>> old = {'foo': 'bar', 'numbers': [1, 3, 4, 8]}
509
+ >>> new = {'baz': 'qux', 'numbers': [1, 4, 7]}
510
+ >>> patch = make_patch(old, new)
511
+ >>> if patch:
512
+ ... # document have changed, do something useful
513
+ ... patch.apply(old) #doctest: +ELLIPSIS
514
+ {...}
515
+ """
516
+ def __init__(self, patch, pointer_cls=JsonPointer):
517
+ self.patch = patch
518
+ self.pointer_cls = pointer_cls
519
+
520
+ # Verify that the structure of the patch document
521
+ # is correct by retrieving each patch element.
522
+ # Much of the validation is done in the initializer
523
+ # though some is delayed until the patch is applied.
524
+ if _fast(type(self), pointer_cls, patch):
525
+ _core.validate(patch, self._check_operation)
526
+ return
527
+ for op in self.patch:
528
+ self._check_operation(op)
529
+
530
+ def _check_operation(self, op):
531
+ # We're only checking for strings in the following check
532
+ # for two reasons:
533
+ #
534
+ # - It should come from JSON, which only allows strings as
535
+ # dictionary keys, so having a string here unambiguously means
536
+ # someone used: {"op": ..., ...} instead of [{"op": ..., ...}].
537
+ #
538
+ # - There's no possible false positive: if someone give a sequence
539
+ # of mappings, this won't raise.
540
+ if isinstance(op, (str, bytes)):
541
+ raise InvalidJsonPatch("Document is expected to be sequence of "
542
+ "operations, got a sequence of strings.")
543
+
544
+ self._get_operation(op)
545
+
546
+ def __str__(self):
547
+ """str(self) -> self.to_string()"""
548
+ return self.to_string()
549
+
550
+ def __bool__(self):
551
+ return bool(self.patch)
552
+
553
+ __nonzero__ = __bool__
554
+
555
+ def __iter__(self):
556
+ return iter(self.patch)
557
+
558
+ def __hash__(self):
559
+ return hash(tuple(self._ops))
560
+
561
+ def __eq__(self, other):
562
+ if not isinstance(other, JsonPatch):
563
+ return False
564
+ return self._ops == other._ops
565
+
566
+ def __ne__(self, other):
567
+ return not(self == other)
568
+
569
+ @classmethod
570
+ def from_string(cls, patch_str, loads=None, pointer_cls=JsonPointer):
571
+ """Creates JsonPatch instance from string source.
572
+
573
+ :param patch_str: JSON patch as raw string.
574
+ :type patch_str: str
575
+
576
+ :param loads: A function of one argument that loads a serialized
577
+ JSON string.
578
+ :type loads: function
579
+
580
+ :param pointer_cls: JSON pointer class to use.
581
+ :type pointer_cls: Type[JsonPointer]
582
+
583
+ :return: :class:`JsonPatch` instance.
584
+ """
585
+ json_loader = loads or cls.json_loader
586
+ patch = json_loader(patch_str)
587
+ return cls(patch, pointer_cls=pointer_cls)
588
+
589
+ @classmethod
590
+ def from_diff(
591
+ cls, src, dst, optimization=True, dumps=None,
592
+ pointer_cls=JsonPointer,
593
+ ):
594
+ """Creates JsonPatch instance based on comparison of two document
595
+ objects. Json patch would be created for `src` argument against `dst`
596
+ one. The resulting patch transforms `src` into `dst`, so it has to be
597
+ applied to `src` (or to a document equal to it).
598
+
599
+ :param src: Data source document object.
600
+ :type src: dict
601
+
602
+ :param dst: Data target document object.
603
+ :type dst: dict
604
+
605
+ :param dumps: A function of one argument that produces a serialized
606
+ JSON string.
607
+ :type dumps: function
608
+
609
+ :param pointer_cls: JSON pointer class to use.
610
+ :type pointer_cls: Type[JsonPointer]
611
+
612
+ :return: :class:`JsonPatch` instance.
613
+
614
+ >>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]}
615
+ >>> dst = {'baz': 'qux', 'numbers': [1, 4, 7]}
616
+ >>> patch = JsonPatch.from_diff(src, dst)
617
+ >>> new = patch.apply(src)
618
+ >>> new == dst
619
+ True
620
+ """
621
+ if dumps is None and cls.json_dumper is json.dumps:
622
+ ops = _core.diff(src, dst)
623
+ if ops is not None:
624
+ if cls.__init__ is JsonPatch.__init__ and _fast(cls, pointer_cls, ops):
625
+ # generated operations need no validation
626
+ patch = cls.__new__(cls)
627
+ patch.patch, patch.pointer_cls = ops, pointer_cls
628
+ return patch
629
+ return cls(ops, pointer_cls=pointer_cls)
630
+
631
+ json_dumper = dumps or cls.json_dumper
632
+ builder = DiffBuilder(src, dst, json_dumper, pointer_cls=pointer_cls)
633
+ builder._compare_values((), None, src, dst)
634
+ ops = list(builder.execute())
635
+ return cls(ops, pointer_cls=pointer_cls)
636
+
637
+ def to_string(self, dumps=None):
638
+ """Returns patch set as JSON string."""
639
+ json_dumper = dumps or self.json_dumper
640
+ return json_dumper(self.patch)
641
+
642
+ @property
643
+ def _ops(self):
644
+ return tuple(map(self._get_operation, self.patch))
645
+
646
+ def apply(self, obj, in_place=False):
647
+ """Applies the patch to a given object.
648
+
649
+ :param obj: Document object.
650
+ :type obj: dict
651
+
652
+ :param in_place: Tweaks the way how patch would be applied - directly to
653
+ specified `obj` or to its copy.
654
+ :type in_place: bool
655
+
656
+ :return: Modified `obj`.
657
+ """
658
+
659
+ if _fast(type(self), self.pointer_cls, self.patch):
660
+ return _core.apply(self.patch, obj, in_place, self._get_operation, _add_stage)
661
+
662
+ if not in_place:
663
+ obj = copy.deepcopy(obj)
664
+
665
+ for operation in self._ops:
666
+ obj = operation.apply(obj)
667
+
668
+ return obj
669
+
670
+ def _get_operation(self, operation):
671
+ if 'op' not in operation:
672
+ raise InvalidJsonPatch("Operation does not contain 'op' member")
673
+
674
+ op = operation['op']
675
+
676
+ if not isinstance(op, (str, bytes)):
677
+ raise InvalidJsonPatch("Operation's op must be a string")
678
+
679
+ if op not in self.operations:
680
+ raise InvalidJsonPatch("Unknown operation {0!r}".format(op))
681
+
682
+ cls = self.operations[op]
683
+ return cls(operation, pointer_cls=self.pointer_cls)
684
+
685
+
686
+ def _fast(cls, pointer_cls, patch):
687
+ """Can the Rust core stand in for the Python code of this patch class?"""
688
+ return (pointer_cls is JsonPointer and type(patch) in (list, tuple)
689
+ and cls.operations is JsonPatch.operations
690
+ and cls._get_operation is JsonPatch._get_operation)
691
+
692
+
693
+ def _add_stage(obj, location, value):
694
+ """The second half of move and copy, for the Rust core to delegate to."""
695
+ return AddOperation({'op': 'add', 'path': location})._add(obj, value)
696
+
697
+
698
+ deepcopy = _core.deepcopy
699
+
700
+
701
+ class DiffBuilder(object):
702
+
703
+ def __init__(self, src_doc, dst_doc, dumps=json.dumps, pointer_cls=JsonPointer):
704
+ self.dumps = dumps
705
+ self.pointer_cls = pointer_cls
706
+ self.index_storage = [{}, {}]
707
+ self.index_storage2 = [[], []]
708
+ self.__root = root = []
709
+ self.src_doc = src_doc
710
+ self.dst_doc = dst_doc
711
+ root[:] = [root, root, None]
712
+
713
+ def store_index(self, value, index, st):
714
+ typed_key = (value, type(value))
715
+ try:
716
+ storage = self.index_storage[st]
717
+ stored = storage.get(typed_key)
718
+ if stored is None:
719
+ storage[typed_key] = [index]
720
+ else:
721
+ storage[typed_key].append(index)
722
+
723
+ except TypeError:
724
+ self.index_storage2[st].append((typed_key, index))
725
+
726
+ def take_index(self, value, st):
727
+ typed_key = (value, type(value))
728
+ try:
729
+ stored = self.index_storage[st].get(typed_key)
730
+ if stored:
731
+ return stored.pop()
732
+
733
+ except TypeError:
734
+ storage = self.index_storage2[st]
735
+ for i in range(len(storage)-1, -1, -1):
736
+ if storage[i][0] == typed_key:
737
+ return storage.pop(i)[1]
738
+
739
+ def insert(self, op):
740
+ root = self.__root
741
+ last = root[0]
742
+ last[1] = root[0] = [last, root, op]
743
+ return root[0]
744
+
745
+ def remove(self, index):
746
+ link_prev, link_next, _ = index
747
+ link_prev[1] = link_next
748
+ link_next[0] = link_prev
749
+ index[:] = []
750
+
751
+ def iter_from(self, start):
752
+ root = self.__root
753
+ curr = start[1]
754
+ while curr is not root:
755
+ yield curr[2]
756
+ curr = curr[1]
757
+
758
+ def __iter__(self):
759
+ root = self.__root
760
+ curr = root[1]
761
+ while curr is not root:
762
+ yield curr[2]
763
+ curr = curr[1]
764
+
765
+ def execute(self):
766
+ root = self.__root
767
+ curr = root[1]
768
+ while curr is not root:
769
+ if curr[1] is not root:
770
+ op_first, op_second = curr[2], curr[1][2]
771
+ if op_first['path'] == op_second['path'] and \
772
+ op_first['op'] == 'remove' and \
773
+ op_second['op'] == 'add':
774
+ yield {
775
+ 'op': 'replace',
776
+ 'path': _to_pointer(op_second['path']),
777
+ 'value': op_second['value'],
778
+ }
779
+ curr = curr[1][1]
780
+ continue
781
+
782
+ operation = dict(curr[2])
783
+ for member in ('from', 'path'):
784
+ if member in operation:
785
+ operation[member] = _to_pointer(operation[member])
786
+ yield operation
787
+ curr = curr[1]
788
+
789
+ def _adjust_following(self, index, removed):
790
+ """ Works out how the operations following index change if the item
791
+ that the operation at index removes (or adds, if not removed) is
792
+ moved there later instead.
793
+
794
+ The operations were made for documents without (or with) the item,
795
+ and are changed to documents with (or without) it. They never refer
796
+ to the item, as the diff does not change values it adds or removes.
797
+ Returns these changes and where the item is after the operations. """
798
+ location = index[2]['path']
799
+ changes = []
800
+ # only array indices change, so nothing does if there are none
801
+ if not any(isinstance(part, int) for part in location):
802
+ return changes, location
803
+
804
+ for op in self.iter_from(index):
805
+ # 'from' is removed before 'path' is added
806
+ for member in ('from', 'path'):
807
+ if member not in op:
808
+ continue
809
+
810
+ inserted = member == 'path' and op['op'] in ('add', 'move')
811
+ if removed:
812
+ parts = _with_item(op[member], location, inserted)
813
+ present = parts
814
+ else:
815
+ parts = _without_item(op[member], location)
816
+ present = op[member]
817
+
818
+ if op['op'] != 'replace':
819
+ location = _item_after(location, present, inserted)
820
+ changes.append((op, member, parts))
821
+
822
+ return changes, location
823
+
824
+ def _item_added(self, path, key, item):
825
+ target = _path_join(path, key)
826
+ index = self.take_index(item, _ST_REMOVE)
827
+ if index is not None:
828
+ changes, source = self._adjust_following(index, removed=True)
829
+ # RFC 6902 does not allow moving a value into its own children
830
+ if not _is_inside(target, source):
831
+ for op, member, parts in changes:
832
+ op[member] = parts
833
+ self.remove(index)
834
+ if source != target:
835
+ self.insert({'op': 'move', 'from': source,
836
+ 'path': target})
837
+ return
838
+
839
+ new_index = self.insert({'op': 'add', 'path': target, 'value': item})
840
+ self.store_index(item, new_index, _ST_ADD)
841
+
842
+ def _item_removed(self, path, key, item):
843
+ source = _path_join(path, key)
844
+ index = self.take_index(item, _ST_ADD)
845
+ if index is not None:
846
+ changes, added = self._adjust_following(index, removed=False)
847
+ moved_from = _without_item(source, added)
848
+ target = _item_after(added, source, False)
849
+ # RFC 6902 does not allow moving a value into its own children
850
+ if not _is_inside(target, moved_from):
851
+ for op, member, parts in changes:
852
+ op[member] = parts
853
+ self.remove(index)
854
+ if moved_from != target:
855
+ self.insert({'op': 'move', 'from': moved_from,
856
+ 'path': target})
857
+ return
858
+
859
+ new_index = self.insert({'op': 'remove', 'path': source})
860
+ self.store_index(item, new_index, _ST_REMOVE)
861
+
862
+ def _item_replaced(self, path, key, item):
863
+ self.insert({
864
+ 'op': 'replace',
865
+ 'path': _path_join(path, key),
866
+ 'value': item,
867
+ })
868
+
869
+ def _compare_dicts(self, path, src, dst):
870
+ added_keys = [key for key in dst if key not in src]
871
+ removed_keys = [key for key in src if key not in dst]
872
+ intersection = [key for key in src if key in dst]
873
+
874
+ for key in removed_keys:
875
+ self._item_removed(path, str(key), src[key])
876
+
877
+ for key in added_keys:
878
+ self._item_added(path, str(key), dst[key])
879
+
880
+ for key in intersection:
881
+ self._compare_values(path, str(key), src[key], dst[key])
882
+
883
+ def _compare_lists(self, path, src, dst):
884
+ len_src, len_dst = len(src), len(dst)
885
+ max_len = max(len_src, len_dst)
886
+ min_len = min(len_src, len_dst)
887
+ for key in range(max_len):
888
+ if key < min_len:
889
+ old, new = src[key], dst[key]
890
+ if isinstance(old, MutableMapping) and \
891
+ isinstance(new, MutableMapping):
892
+ self._compare_dicts(_path_join(path, key), old, new)
893
+
894
+ elif isinstance(old, MutableSequence) and \
895
+ isinstance(new, MutableSequence):
896
+ self._compare_lists(_path_join(path, key), old, new)
897
+
898
+ # To ensure we catch changes to JSON, we can't rely on a
899
+ # simple old == new, because it would not recognize the
900
+ # difference between 1 and True, among other things.
901
+ elif self.dumps(old) == self.dumps(new):
902
+ continue
903
+
904
+ else:
905
+ self._item_removed(path, key, old)
906
+ self._item_added(path, key, new)
907
+
908
+ elif len_src > len_dst:
909
+ self._item_removed(path, len_dst, src[key])
910
+
911
+ else:
912
+ self._item_added(path, key, dst[key])
913
+
914
+ def _compare_values(self, path, key, src, dst):
915
+ if isinstance(src, MutableMapping) and \
916
+ isinstance(dst, MutableMapping):
917
+ self._compare_dicts(_path_join(path, key), src, dst)
918
+
919
+ elif isinstance(src, MutableSequence) and \
920
+ isinstance(dst, MutableSequence):
921
+ self._compare_lists(_path_join(path, key), src, dst)
922
+
923
+ # To ensure we catch changes to JSON, we can't rely on a simple
924
+ # src == dst, because it would not recognize the difference between
925
+ # 1 and True, among other things. Using json.dumps is the most
926
+ # fool-proof way to ensure we catch type changes that matter to JSON
927
+ # and ignore those that don't. The performance of this could be
928
+ # improved by doing more direct type checks, but we'd need to be
929
+ # careful to accept type changes that don't matter when JSONified.
930
+ elif self.dumps(src) == self.dumps(dst):
931
+ return
932
+
933
+ else:
934
+ self._item_replaced(path, key, dst)
935
+
936
+
937
+ # The DiffBuilder keeps locations as tuples of object keys (str) and array
938
+ # indices (int), so it can tell them apart when it adjusts array indices
939
+
940
+
941
+ def _path_join(path, key):
942
+ if key is None:
943
+ return path
944
+
945
+ return path + (key,)
946
+
947
+
948
+ def _to_pointer(path):
949
+ return ''.join('/' + str(part).replace('~', '~0').replace('/', '~1')
950
+ for part in path)
951
+
952
+
953
+ def _is_prefix(sub_parts, parts):
954
+ return sub_parts == parts[:len(sub_parts)]
955
+
956
+
957
+ def _is_inside(parts, container):
958
+ return len(parts) > len(container) and _is_prefix(container, parts)
959
+
960
+
961
+ def _shift(parts, depth, offset):
962
+ return parts[:depth] + (parts[depth] + offset,) + parts[depth + 1:]
963
+
964
+
965
+ def _with_item(parts, location, insertion):
966
+ """ Changes parts, which assumes that there is no item at location, to
967
+ the item being there. insertion tells if parts is where 'add' inserts """
968
+ depth = len(location) - 1
969
+ if isinstance(location[-1], int) and _is_inside(parts, location[:-1]):
970
+ index = parts[depth]
971
+ # inserting at the index of the item inserts before it
972
+ before_item = insertion and len(parts) == len(location) and \
973
+ index == location[-1]
974
+ if index >= location[-1] and not before_item:
975
+ return _shift(parts, depth, 1)
976
+
977
+ return parts
978
+
979
+
980
+ def _without_item(parts, location):
981
+ """ Changes parts, which assumes that there is an item at location, to
982
+ the item missing """
983
+ depth = len(location) - 1
984
+ if isinstance(location[-1], int) and _is_inside(parts, location[:-1]) \
985
+ and parts[depth] > location[-1]:
986
+ return _shift(parts, depth, -1)
987
+
988
+ return parts
989
+
990
+
991
+ def _item_after(location, parts, inserted):
992
+ """ Where the item at location is after inserting (or removing, if not
993
+ inserted) the value at parts """
994
+ depth = len(parts) - 1
995
+ if not isinstance(parts[-1], int) or not _is_inside(location, parts[:-1]):
996
+ return location
997
+
998
+ if inserted and location[depth] >= parts[-1]:
999
+ return _shift(location, depth, 1)
1000
+
1001
+ if not inserted and location[depth] > parts[-1]:
1002
+ return _shift(location, depth, -1)
1003
+
1004
+ return location
1005
+
1006
+
1007
+ def _to_last(pointer, doc):
1008
+ """Resolve pointer like JsonPointer.to_last, without indexing into strings.
1009
+
1010
+ RFC 6901 only allows reference tokens to be applied to objects and arrays,
1011
+ but older versions of jsonpointer treat strings as sequences.
1012
+ """
1013
+ subobj, part = pointer.to_last(doc)
1014
+
1015
+ if part is not None and isinstance(subobj, str):
1016
+ raise JsonPointerException(
1017
+ "Cannot apply token '{0}' to non-container type {1}".format(
1018
+ part, type(subobj)))
1019
+
1020
+ return subobj, part