jsonpatch 1.34__py3-none-any.whl → 1.35__py3-none-any.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.
- {jsonpatch-1.34.dist-info → jsonpatch-1.35.dist-info}/METADATA +17 -16
- jsonpatch-1.35.dist-info/RECORD +9 -0
- jsonpatch-1.35.dist-info/entry_points.txt +3 -0
- jsonpatch-1.35.dist-info/top_level.txt +2 -0
- jsonpatch.py +430 -200
- jsonpatch-1.34.data/scripts/jsonpatch → jsonpatch_cli.py +55 -25
- jsonpatch-1.34.data/scripts/jsondiff +0 -39
- jsonpatch-1.34.dist-info/RECORD +0 -9
- jsonpatch-1.34.dist-info/top_level.txt +0 -1
- {jsonpatch-1.34.dist-info → jsonpatch-1.35.dist-info}/WHEEL +0 -0
- {jsonpatch-1.34.dist-info → jsonpatch-1.35.dist-info}/licenses/AUTHORS +0 -0
- {jsonpatch-1.34.dist-info → jsonpatch-1.35.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jsonpatch
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.35
|
|
4
4
|
Summary: Apply JSON-Patches (RFC 6902)
|
|
5
5
|
Home-page: https://github.com/stefankoegl/python-json-patch
|
|
6
6
|
Author: Stefan Kögl
|
|
7
7
|
Author-email: stefan@skoegl.net
|
|
8
|
-
License:
|
|
8
|
+
License-Expression: BSD-3-Clause
|
|
9
9
|
Project-URL: Website, https://github.com/stefankoegl/python-json-patch
|
|
10
10
|
Project-URL: Repository, https://github.com/stefankoegl/python-json-patch.git
|
|
11
11
|
Project-URL: Documentation, https://python-json-patch.readthedocs.org/
|
|
12
12
|
Project-URL: PyPI, https://pypi.org/pypi/jsonpatch
|
|
13
|
-
Project-URL: Tests, https://
|
|
14
|
-
Project-URL: Test Coverage, https://coveralls.io/
|
|
13
|
+
Project-URL: Tests, https://github.com/stefankoegl/python-json-patch/actions
|
|
14
|
+
Project-URL: Test Coverage, https://coveralls.io/github/stefankoegl/python-json-patch
|
|
15
15
|
Classifier: Development Status :: 5 - Production/Stable
|
|
16
16
|
Classifier: Environment :: Console
|
|
17
17
|
Classifier: Intended Audience :: Developers
|
|
18
|
-
Classifier: License :: OSI Approved :: BSD License
|
|
19
18
|
Classifier: Operating System :: OS Independent
|
|
20
19
|
Classifier: Programming Language :: Python
|
|
21
20
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -23,6 +22,8 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.10
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.11
|
|
25
24
|
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
27
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
27
28
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
28
29
|
Classifier: Topic :: Software Development :: Libraries
|
|
@@ -31,15 +32,15 @@ Requires-Python: >=3.10
|
|
|
31
32
|
Description-Content-Type: text/markdown
|
|
32
33
|
License-File: LICENSE
|
|
33
34
|
License-File: AUTHORS
|
|
34
|
-
Requires-Dist: jsonpointer>=
|
|
35
|
+
Requires-Dist: jsonpointer>=3.2
|
|
35
36
|
Dynamic: author
|
|
36
37
|
Dynamic: author-email
|
|
37
38
|
Dynamic: classifier
|
|
38
39
|
Dynamic: description
|
|
39
40
|
Dynamic: description-content-type
|
|
40
41
|
Dynamic: home-page
|
|
41
|
-
Dynamic: license
|
|
42
42
|
Dynamic: license-file
|
|
43
|
+
Dynamic: license-expression
|
|
43
44
|
Dynamic: project-url
|
|
44
45
|
Dynamic: requires-dist
|
|
45
46
|
Dynamic: requires-python
|
|
@@ -48,29 +49,29 @@ Dynamic: summary
|
|
|
48
49
|
python-json-patch
|
|
49
50
|
=================
|
|
50
51
|
|
|
51
|
-
[](https://pypi.
|
|
52
|
-
[](https://pypi.
|
|
53
|
-
[](https://pypi.org/project/jsonpatch//)
|
|
53
|
+
[](https://pypi.org/project/jsonpatch//)
|
|
54
|
+
[](https://github.com/stefankoegl/python-json-patch/actions/workflows/test.yaml)
|
|
55
|
+
[](https://coveralls.io/github/stefankoegl/python-json-patch?branch=master)
|
|
55
56
|
|
|
56
57
|
Applying JSON Patches in Python
|
|
57
58
|
-------------------------------
|
|
58
59
|
|
|
59
60
|
Library to apply JSON Patches according to
|
|
60
|
-
[RFC 6902](
|
|
61
|
+
[RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902)
|
|
61
62
|
|
|
62
63
|
See source code for examples
|
|
63
64
|
|
|
64
65
|
* Website: https://github.com/stefankoegl/python-json-patch
|
|
65
66
|
* Repository: https://github.com/stefankoegl/python-json-patch.git
|
|
66
67
|
* Documentation: https://python-json-patch.readthedocs.org/
|
|
67
|
-
* PyPI: https://pypi.
|
|
68
|
-
*
|
|
69
|
-
* Coveralls: https://coveralls.io/
|
|
68
|
+
* PyPI: https://pypi.org/project/jsonpatch/
|
|
69
|
+
* CI: https://github.com/stefankoegl/python-json-patch/actions
|
|
70
|
+
* Coveralls: https://coveralls.io/github/stefankoegl/python-json-patch
|
|
70
71
|
|
|
71
72
|
Running external tests
|
|
72
73
|
----------------------
|
|
73
|
-
To run external tests (such as those from https://github.com/json-patch/json-patch-tests) use
|
|
74
|
+
To run external tests (such as those from https://github.com/json-patch/json-patch-tests) use ext_tests.py
|
|
74
75
|
|
|
75
76
|
./ext_tests.py ../json-patch-tests/tests.json
|
|
76
77
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
jsonpatch.py,sha256=fO9Isq-oqu2Ua64PLMzuV3wCVIrx0SW7v9xB84W44rA,42817
|
|
2
|
+
jsonpatch_cli.py,sha256=HD5iqPJP__uTPKm50eg7xbTaTuQHL4Bt7MpgO-vlzaM,5304
|
|
3
|
+
jsonpatch-1.35.dist-info/licenses/AUTHORS,sha256=hOtEnBGbfDegKjzMDEl5vuVJyczbI2PlYE2ho4LjGNQ,140
|
|
4
|
+
jsonpatch-1.35.dist-info/licenses/LICENSE,sha256=Q5PrZzP6HjZ_QXabOC9PG-e_Da6x4kPtkrP12Lr9I28,1481
|
|
5
|
+
jsonpatch-1.35.dist-info/METADATA,sha256=v3G9IpDxUG83tcXWWPX4CQlfs4lMll0QGIw2i9qfDMM,3672
|
|
6
|
+
jsonpatch-1.35.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
7
|
+
jsonpatch-1.35.dist-info/entry_points.txt,sha256=aY9ormBVRZHHDaAeMcdqTBwZ-ax7GvBTSQ33I75xtZM,98
|
|
8
|
+
jsonpatch-1.35.dist-info/top_level.txt,sha256=efUF341HUk8WcQKnFt8ZLKEKNrm1kFT76Flr38Fjno0,24
|
|
9
|
+
jsonpatch-1.35.dist-info/RECORD,,
|
jsonpatch.py
CHANGED
|
@@ -32,11 +32,13 @@
|
|
|
32
32
|
|
|
33
33
|
""" Apply JSON-Patches (RFC 6902) """
|
|
34
34
|
|
|
35
|
+
import bisect
|
|
35
36
|
import collections
|
|
36
37
|
import copy
|
|
37
38
|
import functools
|
|
38
39
|
import json
|
|
39
|
-
from collections.abc import MutableMapping, MutableSequence, Sequence
|
|
40
|
+
from collections.abc import Mapping, MutableMapping, MutableSequence, Sequence
|
|
41
|
+
from difflib import SequenceMatcher
|
|
40
42
|
from types import MappingProxyType
|
|
41
43
|
|
|
42
44
|
from jsonpointer import JsonPointer, JsonPointerException
|
|
@@ -45,12 +47,16 @@ from jsonpointer import JsonPointer, JsonPointerException
|
|
|
45
47
|
_ST_ADD = 0
|
|
46
48
|
_ST_REMOVE = 1
|
|
47
49
|
|
|
50
|
+
# Up to which product of their lengths DiffBuilder matches up all equal items
|
|
51
|
+
# of two lists
|
|
52
|
+
_EXACT_MATCH_LIMIT = 500 ** 2
|
|
53
|
+
|
|
48
54
|
|
|
49
55
|
# Will be parsed by setup.py to determine package metadata
|
|
50
56
|
__author__ = 'Stefan Kögl <stefan@skoegl.net>'
|
|
51
|
-
__version__ = '1.
|
|
57
|
+
__version__ = '1.35'
|
|
52
58
|
__website__ = 'https://github.com/stefankoegl/python-json-patch'
|
|
53
|
-
__license__ = '
|
|
59
|
+
__license__ = 'BSD-3-Clause'
|
|
54
60
|
|
|
55
61
|
|
|
56
62
|
class JsonPatchException(Exception):
|
|
@@ -102,7 +108,7 @@ def apply_patch(doc, patch, in_place=False, pointer_cls=JsonPointer):
|
|
|
102
108
|
:param patch: JSON patch as list of dicts or raw JSON-encoded string.
|
|
103
109
|
:type patch: list or str
|
|
104
110
|
|
|
105
|
-
:param in_place: While
|
|
111
|
+
:param in_place: While ``True`` patch will modify target document.
|
|
106
112
|
By default patch will be applied to document copy.
|
|
107
113
|
:type in_place: bool
|
|
108
114
|
|
|
@@ -180,17 +186,37 @@ class PatchOperation(object):
|
|
|
180
186
|
|
|
181
187
|
self.operation = operation
|
|
182
188
|
|
|
189
|
+
def _from_pointer(self):
|
|
190
|
+
"""Builds the pointer of the 'from' member like __init__ builds the
|
|
191
|
+
one of 'path'."""
|
|
192
|
+
try:
|
|
193
|
+
from_ptr = self.operation['from']
|
|
194
|
+
except KeyError:
|
|
195
|
+
raise InvalidJsonPatch(
|
|
196
|
+
"The operation does not contain a 'from' member")
|
|
197
|
+
|
|
198
|
+
if isinstance(from_ptr, self.pointer_cls):
|
|
199
|
+
return from_ptr
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
return self.pointer_cls(from_ptr)
|
|
203
|
+
except TypeError:
|
|
204
|
+
raise InvalidJsonPatch("Invalid 'from'")
|
|
205
|
+
|
|
183
206
|
def apply(self, obj):
|
|
184
207
|
"""Abstract method that applies a patch operation to the specified object."""
|
|
185
208
|
raise NotImplementedError('should implement the patch operation.')
|
|
186
209
|
|
|
187
210
|
def __hash__(self):
|
|
188
|
-
|
|
211
|
+
# Other members can hold arrays or objects, which are unhashable.
|
|
212
|
+
# Operations that compare equal still have equal 'op' and 'path'.
|
|
213
|
+
return hash((self.operation.get('op'), self.operation['path']))
|
|
189
214
|
|
|
190
215
|
def __eq__(self, other):
|
|
191
216
|
if not isinstance(other, PatchOperation):
|
|
192
217
|
return False
|
|
193
|
-
|
|
218
|
+
# e.g. tests for 1 and for true behave differently
|
|
219
|
+
return _json_equal(self.operation, other.operation)
|
|
194
220
|
|
|
195
221
|
def __ne__(self, other):
|
|
196
222
|
return not(self == other)
|
|
@@ -223,7 +249,10 @@ class RemoveOperation(PatchOperation):
|
|
|
223
249
|
"""Removes an object property or an array element."""
|
|
224
250
|
|
|
225
251
|
def apply(self, obj):
|
|
226
|
-
subobj, part =
|
|
252
|
+
subobj, part = self.pointer.to_last(obj)
|
|
253
|
+
|
|
254
|
+
if part is None:
|
|
255
|
+
raise JsonPatchConflict("can't remove the whole document")
|
|
227
256
|
|
|
228
257
|
if isinstance(subobj, Sequence) and not isinstance(part, int):
|
|
229
258
|
raise JsonPointerException("invalid array index '{0}'".format(part))
|
|
@@ -252,13 +281,13 @@ class AddOperation(PatchOperation):
|
|
|
252
281
|
return self._add(obj, copy.deepcopy(value))
|
|
253
282
|
|
|
254
283
|
def _add(self, obj, value):
|
|
255
|
-
subobj, part =
|
|
284
|
+
subobj, part = self.pointer.to_last(obj)
|
|
256
285
|
|
|
257
|
-
if
|
|
258
|
-
|
|
259
|
-
return value # we're replacing the root
|
|
286
|
+
if part is None:
|
|
287
|
+
return value # we're replacing the root, whatever its type
|
|
260
288
|
|
|
261
|
-
|
|
289
|
+
if isinstance(subobj, MutableSequence):
|
|
290
|
+
if part == '-':
|
|
262
291
|
subobj.append(value) # pylint: disable=E1103
|
|
263
292
|
|
|
264
293
|
elif part > len(subobj) or part < 0:
|
|
@@ -268,16 +297,10 @@ class AddOperation(PatchOperation):
|
|
|
268
297
|
subobj.insert(part, value) # pylint: disable=E1103
|
|
269
298
|
|
|
270
299
|
elif isinstance(subobj, MutableMapping):
|
|
271
|
-
|
|
272
|
-
obj = value # we're replacing the root
|
|
273
|
-
else:
|
|
274
|
-
subobj[part] = value
|
|
300
|
+
subobj[part] = value
|
|
275
301
|
|
|
276
302
|
else:
|
|
277
|
-
|
|
278
|
-
raise TypeError("invalid document type {0}".format(type(subobj)))
|
|
279
|
-
else:
|
|
280
|
-
raise JsonPatchConflict("unable to fully resolve json pointer {0}, part {1}".format(self.location, part))
|
|
303
|
+
raise JsonPatchConflict("unable to fully resolve json pointer {0}, part {1}".format(self.location, part))
|
|
281
304
|
return obj
|
|
282
305
|
|
|
283
306
|
|
|
@@ -294,15 +317,17 @@ class ReplaceOperation(PatchOperation):
|
|
|
294
317
|
# copied for the same reason as in AddOperation.apply
|
|
295
318
|
value = copy.deepcopy(value)
|
|
296
319
|
|
|
297
|
-
subobj, part =
|
|
320
|
+
subobj, part = self.pointer.to_last(obj)
|
|
298
321
|
|
|
299
322
|
if part is None:
|
|
300
323
|
return value
|
|
301
324
|
|
|
302
|
-
if part == "-":
|
|
303
|
-
raise InvalidJsonPatch("'path' with '-' can't be applied to 'replace' operation")
|
|
304
|
-
|
|
305
325
|
if isinstance(subobj, MutableSequence):
|
|
326
|
+
# '-' only refers to the (nonexistent) element after the end of
|
|
327
|
+
# an array; for an object it is an ordinary member name
|
|
328
|
+
if part == "-":
|
|
329
|
+
raise InvalidJsonPatch("'path' with '-' can't be applied to 'replace' operation")
|
|
330
|
+
|
|
306
331
|
if part >= len(subobj) or part < 0:
|
|
307
332
|
raise JsonPatchConflict("can't replace outside of list")
|
|
308
333
|
|
|
@@ -311,10 +336,7 @@ class ReplaceOperation(PatchOperation):
|
|
|
311
336
|
msg = "can't replace a non-existent object '{0}'".format(part)
|
|
312
337
|
raise JsonPatchConflict(msg)
|
|
313
338
|
else:
|
|
314
|
-
|
|
315
|
-
raise TypeError("invalid document type {0}".format(type(subobj)))
|
|
316
|
-
else:
|
|
317
|
-
raise JsonPatchConflict("unable to fully resolve json pointer {0}, part {1}".format(self.location, part))
|
|
339
|
+
raise JsonPatchConflict("unable to fully resolve json pointer {0}, part {1}".format(self.location, part))
|
|
318
340
|
|
|
319
341
|
subobj[part] = value
|
|
320
342
|
return obj
|
|
@@ -324,16 +346,20 @@ class MoveOperation(PatchOperation):
|
|
|
324
346
|
"""Moves an object property or an array element to a new location."""
|
|
325
347
|
|
|
326
348
|
def apply(self, obj):
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
raise
|
|
334
|
-
|
|
349
|
+
from_ptr = self._from_pointer()
|
|
350
|
+
|
|
351
|
+
# Checked before anything is resolved, as removing an array element
|
|
352
|
+
# shifts its siblings, so the target would resolve to a different
|
|
353
|
+
# location. This also covers moving the whole document ('from' is "").
|
|
354
|
+
if self.pointer != from_ptr and self.pointer.contains(from_ptr):
|
|
355
|
+
raise JsonPatchConflict('Cannot move values into their own children')
|
|
356
|
+
|
|
357
|
+
subobj, part = _to_existing_last(from_ptr, obj)
|
|
358
|
+
|
|
359
|
+
# Moving the whole document onto itself is a no-op
|
|
360
|
+
if part is None:
|
|
361
|
+
return obj
|
|
335
362
|
|
|
336
|
-
subobj, part = _to_last(from_ptr, obj)
|
|
337
363
|
try:
|
|
338
364
|
value = subobj[part]
|
|
339
365
|
except (KeyError, IndexError) as ex:
|
|
@@ -343,10 +369,6 @@ class MoveOperation(PatchOperation):
|
|
|
343
369
|
if self.pointer == from_ptr:
|
|
344
370
|
return obj
|
|
345
371
|
|
|
346
|
-
if isinstance(subobj, MutableMapping) and \
|
|
347
|
-
self.pointer.contains(from_ptr):
|
|
348
|
-
raise JsonPatchConflict('Cannot move values into their own children')
|
|
349
|
-
|
|
350
372
|
obj = RemoveOperation({
|
|
351
373
|
'op': 'remove',
|
|
352
374
|
'path': self.operation['from']
|
|
@@ -391,7 +413,7 @@ class TestOperation(PatchOperation):
|
|
|
391
413
|
|
|
392
414
|
def apply(self, obj):
|
|
393
415
|
try:
|
|
394
|
-
subobj, part =
|
|
416
|
+
subobj, part = self.pointer.to_last(obj)
|
|
395
417
|
if part is None:
|
|
396
418
|
val = subobj
|
|
397
419
|
else:
|
|
@@ -405,7 +427,7 @@ class TestOperation(PatchOperation):
|
|
|
405
427
|
raise InvalidJsonPatch(
|
|
406
428
|
"The operation does not contain a 'value' member")
|
|
407
429
|
|
|
408
|
-
if val
|
|
430
|
+
if not _json_equal(val, value):
|
|
409
431
|
msg = '{0} ({1}) is not equal to tested value {2} ({3})'
|
|
410
432
|
raise JsonPatchTestFailed(msg.format(val, type(val),
|
|
411
433
|
value, type(value)))
|
|
@@ -417,13 +439,9 @@ class CopyOperation(PatchOperation):
|
|
|
417
439
|
""" Copies an object property or an array element to a new location """
|
|
418
440
|
|
|
419
441
|
def apply(self, obj):
|
|
420
|
-
|
|
421
|
-
from_ptr = self.pointer_cls(self.operation['from'])
|
|
422
|
-
except KeyError:
|
|
423
|
-
raise InvalidJsonPatch(
|
|
424
|
-
"The operation does not contain a 'from' member")
|
|
442
|
+
from_ptr = self._from_pointer()
|
|
425
443
|
|
|
426
|
-
subobj, part =
|
|
444
|
+
subobj, part = _to_existing_last(from_ptr, obj)
|
|
427
445
|
try:
|
|
428
446
|
value = copy.deepcopy(subobj if part is None else subobj[part])
|
|
429
447
|
except (KeyError, IndexError) as ex:
|
|
@@ -439,18 +457,6 @@ class CopyOperation(PatchOperation):
|
|
|
439
457
|
|
|
440
458
|
|
|
441
459
|
class JsonPatch(object):
|
|
442
|
-
json_dumper = staticmethod(json.dumps)
|
|
443
|
-
json_loader = staticmethod(_jsonloads)
|
|
444
|
-
|
|
445
|
-
operations = MappingProxyType({
|
|
446
|
-
'remove': RemoveOperation,
|
|
447
|
-
'add': AddOperation,
|
|
448
|
-
'replace': ReplaceOperation,
|
|
449
|
-
'move': MoveOperation,
|
|
450
|
-
'test': TestOperation,
|
|
451
|
-
'copy': CopyOperation,
|
|
452
|
-
})
|
|
453
|
-
|
|
454
460
|
"""A JSON Patch is a list of Patch Operations.
|
|
455
461
|
|
|
456
462
|
>>> patch = JsonPatch([
|
|
@@ -496,6 +502,18 @@ class JsonPatch(object):
|
|
|
496
502
|
... patch.apply(old) #doctest: +ELLIPSIS
|
|
497
503
|
{...}
|
|
498
504
|
"""
|
|
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
|
+
|
|
499
517
|
def __init__(self, patch, pointer_cls=JsonPointer):
|
|
500
518
|
self.patch = patch
|
|
501
519
|
self.pointer_cls = pointer_cls
|
|
@@ -552,7 +570,7 @@ class JsonPatch(object):
|
|
|
552
570
|
|
|
553
571
|
:param loads: A function of one argument that loads a serialized
|
|
554
572
|
JSON string.
|
|
555
|
-
:type loads:
|
|
573
|
+
:type loads: Callable
|
|
556
574
|
|
|
557
575
|
:param pointer_cls: JSON pointer class to use.
|
|
558
576
|
:type pointer_cls: Type[JsonPointer]
|
|
@@ -581,7 +599,7 @@ class JsonPatch(object):
|
|
|
581
599
|
|
|
582
600
|
:param dumps: A function of one argument that produces a serialized
|
|
583
601
|
JSON string.
|
|
584
|
-
:type dumps:
|
|
602
|
+
:type dumps: Callable
|
|
585
603
|
|
|
586
604
|
:param pointer_cls: JSON pointer class to use.
|
|
587
605
|
:type pointer_cls: Type[JsonPointer]
|
|
@@ -632,6 +650,9 @@ class JsonPatch(object):
|
|
|
632
650
|
return obj
|
|
633
651
|
|
|
634
652
|
def _get_operation(self, operation):
|
|
653
|
+
if not isinstance(operation, Mapping):
|
|
654
|
+
raise InvalidJsonPatch("Operation must be an object")
|
|
655
|
+
|
|
635
656
|
if 'op' not in operation:
|
|
636
657
|
raise InvalidJsonPatch("Operation does not contain 'op' member")
|
|
637
658
|
|
|
@@ -653,37 +674,32 @@ class DiffBuilder(object):
|
|
|
653
674
|
self.dumps = dumps
|
|
654
675
|
self.pointer_cls = pointer_cls
|
|
655
676
|
self.index_storage = [{}, {}]
|
|
656
|
-
self.index_storage2 = [[], []]
|
|
657
677
|
self.__root = root = []
|
|
658
678
|
self.src_doc = src_doc
|
|
659
679
|
self.dst_doc = dst_doc
|
|
680
|
+
# the _ArrayItems of each array, by the location of the array
|
|
681
|
+
self.arrays = {}
|
|
660
682
|
root[:] = [root, root, None]
|
|
661
683
|
|
|
662
|
-
def
|
|
663
|
-
|
|
684
|
+
def _move_key(self, value):
|
|
685
|
+
""" A key of value, which is equal for values that are equal in JSON,
|
|
686
|
+
so that e.g. [1] is not moved where [true] is added """
|
|
664
687
|
try:
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
688
|
+
# Serialized like in _differing_runs
|
|
689
|
+
return self.dumps(_sorted_members(value))
|
|
690
|
+
except (TypeError, ValueError, RecursionError):
|
|
691
|
+
# Values that cannot be serialized, e.g. as they contain
|
|
692
|
+
# themselves, are only moved where they are added unchanged
|
|
693
|
+
return id(value)
|
|
671
694
|
|
|
672
|
-
|
|
673
|
-
|
|
695
|
+
def store_index(self, value, index, st):
|
|
696
|
+
storage = self.index_storage[st]
|
|
697
|
+
storage.setdefault(self._move_key(value), []).append(index)
|
|
674
698
|
|
|
675
699
|
def take_index(self, value, st):
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
stored
|
|
679
|
-
if stored:
|
|
680
|
-
return stored.pop()
|
|
681
|
-
|
|
682
|
-
except TypeError:
|
|
683
|
-
storage = self.index_storage2[st]
|
|
684
|
-
for i in range(len(storage)-1, -1, -1):
|
|
685
|
-
if storage[i][0] == typed_key:
|
|
686
|
-
return storage.pop(i)[1]
|
|
700
|
+
stored = self.index_storage[st].get(self._move_key(value))
|
|
701
|
+
if stored:
|
|
702
|
+
return stored.pop()
|
|
687
703
|
|
|
688
704
|
def insert(self, op):
|
|
689
705
|
root = self.__root
|
|
@@ -697,13 +713,6 @@ class DiffBuilder(object):
|
|
|
697
713
|
link_next[0] = link_prev
|
|
698
714
|
index[:] = []
|
|
699
715
|
|
|
700
|
-
def iter_from(self, start):
|
|
701
|
-
root = self.__root
|
|
702
|
-
curr = start[1]
|
|
703
|
-
while curr is not root:
|
|
704
|
-
yield curr[2]
|
|
705
|
-
curr = curr[1]
|
|
706
|
-
|
|
707
716
|
def __iter__(self):
|
|
708
717
|
root = self.__root
|
|
709
718
|
curr = root[1]
|
|
@@ -712,11 +721,11 @@ class DiffBuilder(object):
|
|
|
712
721
|
curr = curr[1]
|
|
713
722
|
|
|
714
723
|
def execute(self):
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
while
|
|
718
|
-
if
|
|
719
|
-
op_first, op_second =
|
|
724
|
+
operations = list(self._replay())
|
|
725
|
+
i = 0
|
|
726
|
+
while i < len(operations):
|
|
727
|
+
if i + 1 < len(operations):
|
|
728
|
+
op_first, op_second = operations[i], operations[i + 1]
|
|
720
729
|
if op_first['path'] == op_second['path'] and \
|
|
721
730
|
op_first['op'] == 'remove' and \
|
|
722
731
|
op_second['op'] == 'add':
|
|
@@ -725,93 +734,129 @@ class DiffBuilder(object):
|
|
|
725
734
|
'path': _to_pointer(op_second['path']),
|
|
726
735
|
'value': op_second['value'],
|
|
727
736
|
}
|
|
728
|
-
|
|
737
|
+
i += 2
|
|
729
738
|
continue
|
|
730
739
|
|
|
731
|
-
operation =
|
|
740
|
+
operation = operations[i]
|
|
732
741
|
for member in ('from', 'path'):
|
|
733
742
|
if member in operation:
|
|
734
743
|
operation[member] = _to_pointer(operation[member])
|
|
735
744
|
yield operation
|
|
736
|
-
|
|
745
|
+
i += 1
|
|
746
|
+
|
|
747
|
+
def _replay(self):
|
|
748
|
+
""" Applies the operations to the arrays of the source document, and
|
|
749
|
+
returns them with the array indices at which they apply """
|
|
750
|
+
for items in self.arrays.values():
|
|
751
|
+
items.reset()
|
|
737
752
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
that the operation at index removes (or adds, if not removed) is
|
|
741
|
-
moved there later instead.
|
|
742
|
-
|
|
743
|
-
The operations were made for documents without (or with) the item,
|
|
744
|
-
and are changed to documents with (or without) it. They never refer
|
|
745
|
-
to the item, as the diff does not change values it adds or removes.
|
|
746
|
-
Returns these changes and where the item is after the operations. """
|
|
747
|
-
location = index[2]['path']
|
|
748
|
-
changes = []
|
|
749
|
-
# only array indices change, so nothing does if there are none
|
|
750
|
-
if not any(isinstance(part, int) for part in location):
|
|
751
|
-
return changes, location
|
|
752
|
-
|
|
753
|
-
for op in self.iter_from(index):
|
|
753
|
+
for op in self:
|
|
754
|
+
operation = dict(op)
|
|
754
755
|
# 'from' is removed before 'path' is added
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
756
|
+
if 'from' in op:
|
|
757
|
+
operation['from'] = self._parts(op['from'])
|
|
758
|
+
self._set_present(op['from'], False)
|
|
758
759
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
parts = _without_item(op[member], location)
|
|
765
|
-
present = op[member]
|
|
760
|
+
operation['path'] = self._parts(op['path'])
|
|
761
|
+
if op['op'] == 'remove':
|
|
762
|
+
self._set_present(op['path'], False)
|
|
763
|
+
elif op['op'] in ('add', 'move'):
|
|
764
|
+
self._set_present(op['path'], True)
|
|
766
765
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
766
|
+
yield operation
|
|
767
|
+
|
|
768
|
+
def _array_items(self, location):
|
|
769
|
+
""" The _ArrayItems of the array at location """
|
|
770
|
+
items = self.arrays.get(location)
|
|
771
|
+
if items is None:
|
|
772
|
+
items = self.arrays[location] = _ArrayItems()
|
|
773
|
+
return items
|
|
774
|
+
|
|
775
|
+
def _location(self, parts):
|
|
776
|
+
""" The location of parts in the current document """
|
|
777
|
+
location = ()
|
|
778
|
+
for part in parts:
|
|
779
|
+
if isinstance(part, int):
|
|
780
|
+
part = self._array_items(location).at(part)
|
|
781
|
+
location += (part,)
|
|
782
|
+
return location
|
|
770
783
|
|
|
771
|
-
|
|
784
|
+
def _new_location(self, path, key):
|
|
785
|
+
""" The location of an item inserted at key of the container at path
|
|
786
|
+
of the current document """
|
|
787
|
+
location = self._location(path)
|
|
788
|
+
if isinstance(key, int):
|
|
789
|
+
key = self._array_items(location).insert(key)
|
|
790
|
+
return _path_join(location, key)
|
|
791
|
+
|
|
792
|
+
def _parts(self, location):
|
|
793
|
+
""" The parts of location in the current document. If location is an
|
|
794
|
+
array item that is not in the array, its index is where it would be
|
|
795
|
+
if it was. """
|
|
796
|
+
return tuple(part.index() if isinstance(part, _ArrayItem) else part
|
|
797
|
+
for part in location)
|
|
798
|
+
|
|
799
|
+
def _set_present(self, location, present):
|
|
800
|
+
""" Adds the array item at location to the current document, or
|
|
801
|
+
removes it, if not present """
|
|
802
|
+
item = location[-1] if location else None
|
|
803
|
+
if isinstance(item, _ArrayItem):
|
|
804
|
+
if present:
|
|
805
|
+
item.array.add(item)
|
|
806
|
+
else:
|
|
807
|
+
item.array.remove(item)
|
|
772
808
|
|
|
773
809
|
def _item_added(self, path, key, item):
|
|
774
810
|
target = _path_join(path, key)
|
|
775
811
|
index = self.take_index(item, _ST_REMOVE)
|
|
776
812
|
if index is not None:
|
|
777
|
-
|
|
813
|
+
removed = index[2]['path']
|
|
814
|
+
# where the removed item is if it is not removed
|
|
815
|
+
source = self._parts(removed)
|
|
778
816
|
# RFC 6902 does not allow moving a value into its own children
|
|
779
817
|
if not _is_inside(target, source):
|
|
780
|
-
for op, member, parts in changes:
|
|
781
|
-
op[member] = parts
|
|
782
818
|
self.remove(index)
|
|
783
819
|
if source != target:
|
|
784
|
-
self.insert({'op': 'move', 'from':
|
|
785
|
-
'path':
|
|
820
|
+
self.insert({'op': 'move', 'from': removed,
|
|
821
|
+
'path': self._new_location(path, key)})
|
|
822
|
+
else:
|
|
823
|
+
# the removed item stays where the added one would be
|
|
824
|
+
self._set_present(removed, True)
|
|
786
825
|
return
|
|
787
826
|
|
|
788
|
-
new_index = self.insert({'op': 'add',
|
|
827
|
+
new_index = self.insert({'op': 'add',
|
|
828
|
+
'path': self._new_location(path, key),
|
|
829
|
+
'value': item})
|
|
789
830
|
self.store_index(item, new_index, _ST_ADD)
|
|
790
831
|
|
|
791
832
|
def _item_removed(self, path, key, item):
|
|
792
833
|
source = _path_join(path, key)
|
|
834
|
+
location = self._location(source)
|
|
793
835
|
index = self.take_index(item, _ST_ADD)
|
|
794
836
|
if index is not None:
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
#
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
self.
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
837
|
+
added_location = index[2]['path']
|
|
838
|
+
added = self._parts(added_location)
|
|
839
|
+
# The documents are compared in order, so the added item is in
|
|
840
|
+
# the part compared already: it is in front of the removed one,
|
|
841
|
+
# and neither inside it nor moved by removing it
|
|
842
|
+
self.remove(index)
|
|
843
|
+
if _without_item(source, added) != added:
|
|
844
|
+
self._set_present(location, False)
|
|
845
|
+
self.insert({'op': 'move', 'from': location,
|
|
846
|
+
'path': added_location})
|
|
847
|
+
else:
|
|
848
|
+
# the removed item stays where the added one would be
|
|
849
|
+
self._set_present(added_location, False)
|
|
850
|
+
return
|
|
807
851
|
|
|
808
|
-
|
|
852
|
+
self._set_present(location, False)
|
|
853
|
+
new_index = self.insert({'op': 'remove', 'path': location})
|
|
809
854
|
self.store_index(item, new_index, _ST_REMOVE)
|
|
810
855
|
|
|
811
856
|
def _item_replaced(self, path, key, item):
|
|
812
857
|
self.insert({
|
|
813
858
|
'op': 'replace',
|
|
814
|
-
'path': _path_join(path, key),
|
|
859
|
+
'path': self._location(_path_join(path, key)),
|
|
815
860
|
'value': item,
|
|
816
861
|
})
|
|
817
862
|
|
|
@@ -829,13 +874,79 @@ class DiffBuilder(object):
|
|
|
829
874
|
for key in intersection:
|
|
830
875
|
self._compare_values(path, str(key), src[key], dst[key])
|
|
831
876
|
|
|
877
|
+
def _differing_runs(self, src, dst):
|
|
878
|
+
""" Returns (i1, i2, j1, j2) for each run of items src[i1:i2] that is
|
|
879
|
+
replaced by dst[j1:j2], between runs of items that both lists have.
|
|
880
|
+
|
|
881
|
+
Without matching up the items they both have, inserting an item in
|
|
882
|
+
front of arrays or objects would change all of them that follow. """
|
|
883
|
+
try:
|
|
884
|
+
# Serialized like in _compare_values, so e.g. 1 and True differ,
|
|
885
|
+
# but independent of the order of object members
|
|
886
|
+
src_keys = [self.dumps(_sorted_members(item)) for item in src]
|
|
887
|
+
dst_keys = [self.dumps(_sorted_members(item)) for item in dst]
|
|
888
|
+
except (TypeError, ValueError, RecursionError):
|
|
889
|
+
# Items that cannot be serialized are compared by position
|
|
890
|
+
return [(0, len(src), 0, len(dst))]
|
|
891
|
+
|
|
892
|
+
# Only what is between the common start and end is matched up
|
|
893
|
+
shorter = min(len(src), len(dst))
|
|
894
|
+
start = 0
|
|
895
|
+
while start < shorter and src_keys[start] == dst_keys[start]:
|
|
896
|
+
start += 1
|
|
897
|
+
common_end = 0
|
|
898
|
+
while common_end < shorter - start and \
|
|
899
|
+
src_keys[-1 - common_end] == dst_keys[-1 - common_end]:
|
|
900
|
+
common_end += 1
|
|
901
|
+
src_end, dst_end = len(src) - common_end, len(dst) - common_end
|
|
902
|
+
|
|
903
|
+
# How many operations replacing src[i1:i2] by dst[j1:j2] takes
|
|
904
|
+
def cost(i1, i2, j1, j2):
|
|
905
|
+
return abs((i2 - i1) - (j2 - j1)) + sum(
|
|
906
|
+
_positional_cost(src[i], dst[j])
|
|
907
|
+
for i, j in zip(range(i1, i2), range(j1, j2))
|
|
908
|
+
if src_keys[i] != dst_keys[j])
|
|
909
|
+
|
|
910
|
+
by_position = (start, src_end, start, dst_end)
|
|
911
|
+
position_cost = cost(*by_position)
|
|
912
|
+
|
|
913
|
+
# Matching up all items can take time proportional to the product of
|
|
914
|
+
# the list lengths. For longer lists SequenceMatcher does not match
|
|
915
|
+
# up items that are frequent in them, unless next to other matches
|
|
916
|
+
exact = (src_end - start) * (dst_end - start) <= _EXACT_MATCH_LIMIT
|
|
917
|
+
if not exact:
|
|
918
|
+
# It still compares each item with the equal items of the other
|
|
919
|
+
# list that are not frequent, which takes quadratic time for long
|
|
920
|
+
# lists of repeated items. Comparing by position takes quadratic
|
|
921
|
+
# time in its operations as well, as the diff turns them into
|
|
922
|
+
# moves, so matching up may only take about as long
|
|
923
|
+
counts = collections.Counter(dst_keys[start:dst_end])
|
|
924
|
+
comparisons = sum(counts[key] for key in src_keys[start:src_end])
|
|
925
|
+
if comparisons > max(_EXACT_MATCH_LIMIT, position_cost ** 2):
|
|
926
|
+
return [by_position]
|
|
927
|
+
|
|
928
|
+
matcher = SequenceMatcher(None, src_keys[start:src_end],
|
|
929
|
+
dst_keys[start:dst_end], autojunk=not exact)
|
|
930
|
+
runs = [(start + i1, start + i2, start + j1, start + j2)
|
|
931
|
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes()
|
|
932
|
+
if tag != 'equal']
|
|
933
|
+
|
|
934
|
+
# Items can be equal by chance, and matching them up can take more
|
|
935
|
+
# operations than comparing by position: for [2, 3, 5] and [3, 3, 4]
|
|
936
|
+
# it removes 2, replaces 5 and adds 4 instead of replacing 2 and 5.
|
|
937
|
+
# It can also pair up arrays or objects that differ in more members.
|
|
938
|
+
if sum(cost(*run) for run in runs) > position_cost:
|
|
939
|
+
return [by_position]
|
|
940
|
+
|
|
941
|
+
return runs
|
|
942
|
+
|
|
832
943
|
def _compare_lists(self, path, src, dst):
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
old, new = src[key], dst[key]
|
|
944
|
+
# The list has become dst[:j1] + src[i1:] when a run is compared, so
|
|
945
|
+
# src[i1] is at index j1
|
|
946
|
+
for i1, i2, j1, j2 in self._differing_runs(src, dst):
|
|
947
|
+
paired = min(i2 - i1, j2 - j1)
|
|
948
|
+
for key in range(j1, j1 + paired):
|
|
949
|
+
old, new = src[i1 + key - j1], dst[key]
|
|
839
950
|
if isinstance(old, MutableMapping) and \
|
|
840
951
|
isinstance(new, MutableMapping):
|
|
841
952
|
self._compare_dicts(_path_join(path, key), old, new)
|
|
@@ -854,10 +965,10 @@ class DiffBuilder(object):
|
|
|
854
965
|
self._item_removed(path, key, old)
|
|
855
966
|
self._item_added(path, key, new)
|
|
856
967
|
|
|
857
|
-
|
|
858
|
-
self._item_removed(path,
|
|
968
|
+
for old in src[i1 + paired:i2]:
|
|
969
|
+
self._item_removed(path, j1 + paired, old)
|
|
859
970
|
|
|
860
|
-
|
|
971
|
+
for key in range(j1 + paired, j2):
|
|
861
972
|
self._item_added(path, key, dst[key])
|
|
862
973
|
|
|
863
974
|
def _compare_values(self, path, key, src, dst):
|
|
@@ -883,8 +994,160 @@ class DiffBuilder(object):
|
|
|
883
994
|
self._item_replaced(path, key, dst)
|
|
884
995
|
|
|
885
996
|
|
|
886
|
-
|
|
887
|
-
|
|
997
|
+
def _positional_cost(old, new):
|
|
998
|
+
""" Estimates how many operations change old into new, if the items of
|
|
999
|
+
arrays are compared by position """
|
|
1000
|
+
if old == new:
|
|
1001
|
+
return 0
|
|
1002
|
+
if isinstance(old, MutableMapping) and isinstance(new, MutableMapping):
|
|
1003
|
+
return sum(_positional_cost(old[key], new[key]) if key in new else 1
|
|
1004
|
+
for key in old) + sum(key not in old for key in new)
|
|
1005
|
+
if isinstance(old, MutableSequence) and isinstance(new, MutableSequence):
|
|
1006
|
+
return abs(len(old) - len(new)) + sum(
|
|
1007
|
+
_positional_cost(*items) for items in zip(old, new))
|
|
1008
|
+
return 1
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def _json_equal(first, second):
|
|
1012
|
+
""" Compares values the way RFC 6902, 4.6 does: numbers by value, but
|
|
1013
|
+
literals like true only with themselves, which Python considers equal to
|
|
1014
|
+
1, and arrays and objects member by member """
|
|
1015
|
+
if isinstance(first, bool) or isinstance(second, bool):
|
|
1016
|
+
return isinstance(first, bool) and isinstance(second, bool) and \
|
|
1017
|
+
first == second
|
|
1018
|
+
if isinstance(first, MutableMapping) and \
|
|
1019
|
+
isinstance(second, MutableMapping):
|
|
1020
|
+
return len(first) == len(second) and all(
|
|
1021
|
+
key in second and _json_equal(first[key], second[key])
|
|
1022
|
+
for key in first)
|
|
1023
|
+
if isinstance(first, MutableSequence) and \
|
|
1024
|
+
isinstance(second, MutableSequence):
|
|
1025
|
+
return len(first) == len(second) and all(
|
|
1026
|
+
_json_equal(*items) for items in zip(first, second))
|
|
1027
|
+
return first == second
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
def _sorted_members(value):
|
|
1031
|
+
""" Copies the arrays and objects in value, with the members of objects
|
|
1032
|
+
sorted by key """
|
|
1033
|
+
# Most values are scalars, so they are checked for first
|
|
1034
|
+
if value is None or isinstance(value, (str, int, float)):
|
|
1035
|
+
return value
|
|
1036
|
+
if isinstance(value, MutableMapping):
|
|
1037
|
+
return {key: _sorted_members(value[key]) for key in sorted(value)}
|
|
1038
|
+
if isinstance(value, MutableSequence):
|
|
1039
|
+
return [_sorted_members(item) for item in value]
|
|
1040
|
+
return value
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
# The DiffBuilder works with parts, which are tuples of object keys (str) and
|
|
1044
|
+
# array indices (int), and with locations, in which _ArrayItem objects take
|
|
1045
|
+
# the place of array indices. Operations keep locations: when a move replaces
|
|
1046
|
+
# an earlier 'add' or 'remove', the array indices in the operations after it
|
|
1047
|
+
# change, but their locations do not.
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
class _ArrayItem(object):
|
|
1051
|
+
""" An item that an array has at some point of the diff """
|
|
1052
|
+
|
|
1053
|
+
__slots__ = ('array', 'label', 'initial')
|
|
1054
|
+
|
|
1055
|
+
def __init__(self, array, label, initial):
|
|
1056
|
+
self.array = array
|
|
1057
|
+
self.label = label
|
|
1058
|
+
# if the item is in the array of the source document
|
|
1059
|
+
self.initial = initial
|
|
1060
|
+
|
|
1061
|
+
def index(self):
|
|
1062
|
+
""" The index of the item in the array, or the index it would have if
|
|
1063
|
+
it was in the array """
|
|
1064
|
+
return bisect.bisect_left(self.array.present_labels, self.label)
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
class _ArrayItems(object):
|
|
1068
|
+
""" All items that an array has at some point of the diff, and the items
|
|
1069
|
+
that it has at the moment, both ordered by their labels.
|
|
1070
|
+
|
|
1071
|
+
The array has its items in this order at every point of the diff, also
|
|
1072
|
+
when a move later replaces the 'add' or 'remove' of an item. So an item
|
|
1073
|
+
inserted at an index is put directly after the item before it in the
|
|
1074
|
+
array: inserting at the index of a removed item inserts before it, if a
|
|
1075
|
+
move replaces its 'remove' later. """
|
|
1076
|
+
|
|
1077
|
+
# how far apart the labels of items inserted one after the other are
|
|
1078
|
+
step = 1 << 16
|
|
1079
|
+
# how far apart labels are at the start and at the ends
|
|
1080
|
+
gap = 1 << 32
|
|
1081
|
+
|
|
1082
|
+
def __init__(self):
|
|
1083
|
+
self.items = []
|
|
1084
|
+
self.labels = []
|
|
1085
|
+
self.present = []
|
|
1086
|
+
self.present_labels = []
|
|
1087
|
+
|
|
1088
|
+
def at(self, index):
|
|
1089
|
+
""" The item at index of the array """
|
|
1090
|
+
# the items of the source document follow the items that the diff
|
|
1091
|
+
# has dealt with, so they are only created when needed
|
|
1092
|
+
while len(self.present) <= index:
|
|
1093
|
+
label = self.labels[-1] + self.gap if self.labels else 0
|
|
1094
|
+
item = _ArrayItem(self, label, True)
|
|
1095
|
+
self.items.append(item)
|
|
1096
|
+
self.labels.append(label)
|
|
1097
|
+
self.present.append(item)
|
|
1098
|
+
self.present_labels.append(label)
|
|
1099
|
+
|
|
1100
|
+
return self.present[index]
|
|
1101
|
+
|
|
1102
|
+
def insert(self, index):
|
|
1103
|
+
""" Inserts a new item at index of the array, and returns it """
|
|
1104
|
+
position = 0
|
|
1105
|
+
if index:
|
|
1106
|
+
position = bisect.bisect_right(self.labels,
|
|
1107
|
+
self.at(index - 1).label)
|
|
1108
|
+
|
|
1109
|
+
if position == 0 or position == len(self.labels):
|
|
1110
|
+
if not self.labels:
|
|
1111
|
+
label = 0
|
|
1112
|
+
elif position == 0:
|
|
1113
|
+
label = self.labels[0] - self.gap
|
|
1114
|
+
else:
|
|
1115
|
+
label = self.labels[-1] + self.gap
|
|
1116
|
+
|
|
1117
|
+
else:
|
|
1118
|
+
if self.labels[position] - self.labels[position - 1] < 2:
|
|
1119
|
+
self._relabel()
|
|
1120
|
+
low, high = self.labels[position - 1], self.labels[position]
|
|
1121
|
+
label = low + min(self.step, (high - low) // 2)
|
|
1122
|
+
|
|
1123
|
+
item = _ArrayItem(self, label, False)
|
|
1124
|
+
self.items.insert(position, item)
|
|
1125
|
+
self.labels.insert(position, label)
|
|
1126
|
+
self.add(item)
|
|
1127
|
+
return item
|
|
1128
|
+
|
|
1129
|
+
def _relabel(self):
|
|
1130
|
+
self.labels = [i * self.gap for i in range(len(self.items))]
|
|
1131
|
+
for item, label in zip(self.items, self.labels):
|
|
1132
|
+
item.label = label
|
|
1133
|
+
self.present_labels = [item.label for item in self.present]
|
|
1134
|
+
|
|
1135
|
+
def add(self, item):
|
|
1136
|
+
""" Adds item to the array """
|
|
1137
|
+
position = bisect.bisect_left(self.present_labels, item.label)
|
|
1138
|
+
self.present.insert(position, item)
|
|
1139
|
+
self.present_labels.insert(position, item.label)
|
|
1140
|
+
|
|
1141
|
+
def remove(self, item):
|
|
1142
|
+
""" Removes item from the array """
|
|
1143
|
+
position = bisect.bisect_left(self.present_labels, item.label)
|
|
1144
|
+
del self.present[position]
|
|
1145
|
+
del self.present_labels[position]
|
|
1146
|
+
|
|
1147
|
+
def reset(self):
|
|
1148
|
+
""" Changes the array back to its items in the source document """
|
|
1149
|
+
self.present = [item for item in self.items if item.initial]
|
|
1150
|
+
self.present_labels = [item.label for item in self.present]
|
|
888
1151
|
|
|
889
1152
|
|
|
890
1153
|
def _path_join(path, key):
|
|
@@ -911,21 +1174,6 @@ def _shift(parts, depth, offset):
|
|
|
911
1174
|
return parts[:depth] + (parts[depth] + offset,) + parts[depth + 1:]
|
|
912
1175
|
|
|
913
1176
|
|
|
914
|
-
def _with_item(parts, location, insertion):
|
|
915
|
-
""" Changes parts, which assumes that there is no item at location, to
|
|
916
|
-
the item being there. insertion tells if parts is where 'add' inserts """
|
|
917
|
-
depth = len(location) - 1
|
|
918
|
-
if isinstance(location[-1], int) and _is_inside(parts, location[:-1]):
|
|
919
|
-
index = parts[depth]
|
|
920
|
-
# inserting at the index of the item inserts before it
|
|
921
|
-
before_item = insertion and len(parts) == len(location) and \
|
|
922
|
-
index == location[-1]
|
|
923
|
-
if index >= location[-1] and not before_item:
|
|
924
|
-
return _shift(parts, depth, 1)
|
|
925
|
-
|
|
926
|
-
return parts
|
|
927
|
-
|
|
928
|
-
|
|
929
1177
|
def _without_item(parts, location):
|
|
930
1178
|
""" Changes parts, which assumes that there is an item at location, to
|
|
931
1179
|
the item missing """
|
|
@@ -937,33 +1185,15 @@ def _without_item(parts, location):
|
|
|
937
1185
|
return parts
|
|
938
1186
|
|
|
939
1187
|
|
|
940
|
-
def
|
|
941
|
-
"""
|
|
942
|
-
|
|
943
|
-
depth = len(parts) - 1
|
|
944
|
-
if not isinstance(parts[-1], int) or not _is_inside(location, parts[:-1]):
|
|
945
|
-
return location
|
|
946
|
-
|
|
947
|
-
if inserted and location[depth] >= parts[-1]:
|
|
948
|
-
return _shift(location, depth, 1)
|
|
949
|
-
|
|
950
|
-
if not inserted and location[depth] > parts[-1]:
|
|
951
|
-
return _shift(location, depth, -1)
|
|
952
|
-
|
|
953
|
-
return location
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
def _to_last(pointer, doc):
|
|
957
|
-
"""Resolve pointer like JsonPointer.to_last, without indexing into strings.
|
|
1188
|
+
def _to_existing_last(pointer, doc):
|
|
1189
|
+
"""Resolve pointer like JsonPointer.to_last, for a location that has to
|
|
1190
|
+
exist.
|
|
958
1191
|
|
|
959
|
-
|
|
960
|
-
but older versions of jsonpointer treat strings as sequences.
|
|
1192
|
+
The '-' of an array refers to the nonexistent element after its last one.
|
|
961
1193
|
"""
|
|
962
1194
|
subobj, part = pointer.to_last(doc)
|
|
963
1195
|
|
|
964
|
-
if part
|
|
965
|
-
raise JsonPointerException(
|
|
966
|
-
"Cannot apply token '{0}' to non-container type {1}".format(
|
|
967
|
-
part, type(subobj)))
|
|
1196
|
+
if part == '-' and isinstance(subobj, Sequence):
|
|
1197
|
+
raise JsonPointerException("invalid array index '{0}'".format(part))
|
|
968
1198
|
|
|
969
1199
|
return subobj, part
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
#!python
|
|
2
1
|
# -*- coding: utf-8 -*-
|
|
3
2
|
|
|
3
|
+
""" The jsondiff and jsonpatch command line programs """
|
|
4
|
+
|
|
4
5
|
import sys
|
|
5
6
|
import os.path
|
|
6
7
|
import json
|
|
@@ -9,25 +10,56 @@ import tempfile
|
|
|
9
10
|
import argparse
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
jsondiff_parser = argparse.ArgumentParser(
|
|
14
|
+
prog='jsondiff', description='Diff two JSON files')
|
|
15
|
+
jsondiff_parser.add_argument('FILE1', type=argparse.FileType('r'))
|
|
16
|
+
jsondiff_parser.add_argument('FILE2', type=argparse.FileType('r'))
|
|
17
|
+
jsondiff_parser.add_argument('--indent', type=int, default=None,
|
|
18
|
+
help='Indent output by n spaces')
|
|
19
|
+
jsondiff_parser.add_argument('-u', '--preserve-unicode', action='store_true',
|
|
20
|
+
help='Output Unicode character as-is without using Code Point')
|
|
21
|
+
jsondiff_parser.add_argument('-v', '--version', action='version',
|
|
22
|
+
version='%(prog)s ' + jsonpatch.__version__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def jsondiff_main():
|
|
26
|
+
try:
|
|
27
|
+
diff_files()
|
|
28
|
+
except KeyboardInterrupt:
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def diff_files():
|
|
33
|
+
""" Diffs two JSON files and prints a patch """
|
|
34
|
+
args = jsondiff_parser.parse_args()
|
|
35
|
+
doc1 = json.load(args.FILE1)
|
|
36
|
+
doc2 = json.load(args.FILE2)
|
|
37
|
+
patch = jsonpatch.make_patch(doc1, doc2)
|
|
38
|
+
if patch.patch:
|
|
39
|
+
print(json.dumps(patch.patch, indent=args.indent, ensure_ascii=not(args.preserve_unicode)))
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
jsonpatch_parser = argparse.ArgumentParser(
|
|
44
|
+
prog='jsonpatch',
|
|
13
45
|
description='Apply a JSON patch on a JSON file')
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def
|
|
46
|
+
jsonpatch_parser.add_argument('ORIGINAL', type=argparse.FileType('r'),
|
|
47
|
+
help='Original file')
|
|
48
|
+
jsonpatch_parser.add_argument('PATCH', type=argparse.FileType('r'),
|
|
49
|
+
nargs='?', default=sys.stdin,
|
|
50
|
+
help='Patch file (read from stdin if omitted)')
|
|
51
|
+
jsonpatch_parser.add_argument('--indent', type=int, default=None,
|
|
52
|
+
help='Indent output by n spaces')
|
|
53
|
+
jsonpatch_parser.add_argument('-b', '--backup', action='store_true',
|
|
54
|
+
help='Back up ORIGINAL if modifying in-place')
|
|
55
|
+
jsonpatch_parser.add_argument('-i', '--in-place', action='store_true',
|
|
56
|
+
help='Modify ORIGINAL in-place instead of to stdout')
|
|
57
|
+
jsonpatch_parser.add_argument('-v', '--version', action='version',
|
|
58
|
+
version='%(prog)s ' + jsonpatch.__version__)
|
|
59
|
+
jsonpatch_parser.add_argument('-u', '--preserve-unicode', action='store_true',
|
|
60
|
+
help='Output Unicode character as-is without using Code Point')
|
|
61
|
+
|
|
62
|
+
def jsonpatch_main():
|
|
31
63
|
try:
|
|
32
64
|
patch_files()
|
|
33
65
|
except KeyboardInterrupt:
|
|
@@ -35,13 +67,15 @@ def main():
|
|
|
35
67
|
|
|
36
68
|
|
|
37
69
|
def patch_files():
|
|
38
|
-
"""
|
|
39
|
-
args =
|
|
70
|
+
""" Applies a JSON patch on a JSON file """
|
|
71
|
+
args = jsonpatch_parser.parse_args()
|
|
40
72
|
doc = json.load(args.ORIGINAL)
|
|
41
73
|
patch = json.load(args.PATCH)
|
|
42
74
|
result = jsonpatch.apply_patch(doc, patch)
|
|
43
75
|
|
|
44
76
|
if args.in_place:
|
|
77
|
+
# 2026-10-11: Release the input handle before replacing it on Windows.
|
|
78
|
+
args.ORIGINAL.close()
|
|
45
79
|
dirname = os.path.abspath(os.path.dirname(args.ORIGINAL.name))
|
|
46
80
|
|
|
47
81
|
try:
|
|
@@ -101,7 +135,3 @@ def patch_files():
|
|
|
101
135
|
|
|
102
136
|
os.unlink(args.ORIGINAL.name)
|
|
103
137
|
os.rename(pathname, args.ORIGINAL.name)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if __name__ == "__main__":
|
|
107
|
-
main()
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
#!python
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
|
|
4
|
-
import sys
|
|
5
|
-
import json
|
|
6
|
-
import jsonpatch
|
|
7
|
-
import argparse
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
parser = argparse.ArgumentParser(description='Diff two JSON files')
|
|
11
|
-
parser.add_argument('FILE1', type=argparse.FileType('r'))
|
|
12
|
-
parser.add_argument('FILE2', type=argparse.FileType('r'))
|
|
13
|
-
parser.add_argument('--indent', type=int, default=None,
|
|
14
|
-
help='Indent output by n spaces')
|
|
15
|
-
parser.add_argument('-u', '--preserve-unicode', action='store_true',
|
|
16
|
-
help='Output Unicode character as-is without using Code Point')
|
|
17
|
-
parser.add_argument('-v', '--version', action='version',
|
|
18
|
-
version='%(prog)s ' + jsonpatch.__version__)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def main():
|
|
22
|
-
try:
|
|
23
|
-
diff_files()
|
|
24
|
-
except KeyboardInterrupt:
|
|
25
|
-
sys.exit(1)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def diff_files():
|
|
29
|
-
""" Diffs two JSON files and prints a patch """
|
|
30
|
-
args = parser.parse_args()
|
|
31
|
-
doc1 = json.load(args.FILE1)
|
|
32
|
-
doc2 = json.load(args.FILE2)
|
|
33
|
-
patch = jsonpatch.make_patch(doc1, doc2)
|
|
34
|
-
if patch.patch:
|
|
35
|
-
print(json.dumps(patch.patch, indent=args.indent, ensure_ascii=not(args.preserve_unicode)))
|
|
36
|
-
sys.exit(1)
|
|
37
|
-
|
|
38
|
-
if __name__ == "__main__":
|
|
39
|
-
main()
|
jsonpatch-1.34.dist-info/RECORD
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
jsonpatch.py,sha256=fwTHkealtmPMEfKAEW0uHpyWixn4XnQOOL8mmkH4E0g,32582
|
|
2
|
-
jsonpatch-1.34.data/scripts/jsondiff,sha256=pJy1A-g4ON2Nf7SVfjE597OaBbMe4Yp_UZNWBUnrlPQ,1148
|
|
3
|
-
jsonpatch-1.34.data/scripts/jsonpatch,sha256=qTdum0oHcrMmJE1Da-h-sfUHQEYQBp1XQ-QD0D16dHg,3848
|
|
4
|
-
jsonpatch-1.34.dist-info/licenses/AUTHORS,sha256=hOtEnBGbfDegKjzMDEl5vuVJyczbI2PlYE2ho4LjGNQ,140
|
|
5
|
-
jsonpatch-1.34.dist-info/licenses/LICENSE,sha256=Q5PrZzP6HjZ_QXabOC9PG-e_Da6x4kPtkrP12Lr9I28,1481
|
|
6
|
-
jsonpatch-1.34.dist-info/METADATA,sha256=u0wy9hYBgDw9MMpQNUFn2cxr6ZbRYcFhbE1PiOjVdNo,3523
|
|
7
|
-
jsonpatch-1.34.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
-
jsonpatch-1.34.dist-info/top_level.txt,sha256=zcviI7Mh4AvvPBpgKZBzEdgbVh_ewsfvS2t3-wcsnN4,10
|
|
9
|
-
jsonpatch-1.34.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
jsonpatch
|
|
File without changes
|
|
File without changes
|
|
File without changes
|