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.
- fastjsonpatch/__init__.py +1020 -0
- fastjsonpatch/_core.pyd +0 -0
- fastjsonpatch-0.1.0.dist-info/METADATA +200 -0
- fastjsonpatch-0.1.0.dist-info/RECORD +8 -0
- fastjsonpatch-0.1.0.dist-info/WHEEL +4 -0
- fastjsonpatch-0.1.0.dist-info/licenses/LICENSE +15 -0
- fastjsonpatch-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +258 -0
- fastjsonpatch-0.1.0.dist-info/sboms/fastjsonpatch.cyclonedx.json +708 -0
|
@@ -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
|