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.
@@ -1,21 +1,20 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsonpatch
3
- Version: 1.34
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: Modified BSD 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://travis-ci.org/stefankoegl/python-json-patch
14
- Project-URL: Test Coverage, https://coveralls.io/r/stefankoegl/python-json-patch
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>=1.9
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
- [![PyPI version](https://img.shields.io/pypi/v/jsonpatch.svg)](https://pypi.python.org/pypi/jsonpatch/)
52
- [![Supported Python versions](https://img.shields.io/pypi/pyversions/jsonpatch.svg)](https://pypi.python.org/pypi/jsonpatch/)
53
- [![Build Status](https://travis-ci.org/stefankoegl/python-json-patch.png?branch=master)](https://travis-ci.org/stefankoegl/python-json-patch)
54
- [![Coverage Status](https://coveralls.io/repos/stefankoegl/python-json-patch/badge.png?branch=master)](https://coveralls.io/r/stefankoegl/python-json-patch?branch=master)
52
+ [![PyPI version](https://img.shields.io/pypi/v/jsonpatch.svg)](https://pypi.org/project/jsonpatch//)
53
+ [![Supported Python versions](https://img.shields.io/pypi/pyversions/jsonpatch.svg)](https://pypi.org/project/jsonpatch//)
54
+ [![Build Status](https://github.com/stefankoegl/python-json-patch/actions/workflows/test.yaml/badge.svg?branch=master)](https://github.com/stefankoegl/python-json-patch/actions/workflows/test.yaml)
55
+ [![Coverage Status](https://coveralls.io/repos/github/stefankoegl/python-json-patch/badge.svg?branch=master)](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](http://tools.ietf.org/html/rfc6902)
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.python.org/pypi/jsonpatch
68
- * Travis CI: https://travis-ci.org/stefankoegl/python-json-patch
69
- * Coveralls: https://coveralls.io/r/stefankoegl/python-json-patch
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 ext_test.py
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,,
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ jsondiff = jsonpatch_cli:jsondiff_main
3
+ jsonpatch = jsonpatch_cli:jsonpatch_main
@@ -0,0 +1,2 @@
1
+ jsonpatch
2
+ jsonpatch_cli
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.34'
57
+ __version__ = '1.35'
52
58
  __website__ = 'https://github.com/stefankoegl/python-json-patch'
53
- __license__ = 'Modified BSD 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 :const:`True` patch will modify target document.
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
- return hash(frozenset(self.operation.items()))
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
- return self.operation == other.operation
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 = _to_last(self.pointer, obj)
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 = _to_last(self.pointer, obj)
284
+ subobj, part = self.pointer.to_last(obj)
256
285
 
257
- if isinstance(subobj, MutableSequence):
258
- if part is None:
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
- elif part == '-':
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
- if part is None:
272
- obj = value # we're replacing the root
273
- else:
274
- subobj[part] = value
300
+ subobj[part] = value
275
301
 
276
302
  else:
277
- if part is None:
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 = _to_last(self.pointer, obj)
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
- if part is None:
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
- try:
328
- if isinstance(self.operation['from'], self.pointer_cls):
329
- from_ptr = self.operation['from']
330
- else:
331
- from_ptr = self.pointer_cls(self.operation['from'])
332
- except KeyError:
333
- raise InvalidJsonPatch(
334
- "The operation does not contain a 'from' member")
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 = _to_last(self.pointer, obj)
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 != value:
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
- try:
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 = _to_last(from_ptr, obj)
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: function
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: function
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 store_index(self, value, index, st):
663
- typed_key = (value, type(value))
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
- storage = self.index_storage[st]
666
- stored = storage.get(typed_key)
667
- if stored is None:
668
- storage[typed_key] = [index]
669
- else:
670
- storage[typed_key].append(index)
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
- except TypeError:
673
- self.index_storage2[st].append((typed_key, index))
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
- typed_key = (value, type(value))
677
- try:
678
- stored = self.index_storage[st].get(typed_key)
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
- root = self.__root
716
- curr = root[1]
717
- while curr is not root:
718
- if curr[1] is not root:
719
- op_first, op_second = curr[2], curr[1][2]
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
- curr = curr[1][1]
737
+ i += 2
729
738
  continue
730
739
 
731
- operation = dict(curr[2])
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
- curr = curr[1]
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
- def _adjust_following(self, index, removed):
739
- """ Works out how the operations following index change if the item
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
- for member in ('from', 'path'):
756
- if member not in op:
757
- continue
756
+ if 'from' in op:
757
+ operation['from'] = self._parts(op['from'])
758
+ self._set_present(op['from'], False)
758
759
 
759
- inserted = member == 'path' and op['op'] in ('add', 'move')
760
- if removed:
761
- parts = _with_item(op[member], location, inserted)
762
- present = parts
763
- else:
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
- if op['op'] != 'replace':
768
- location = _item_after(location, present, inserted)
769
- changes.append((op, member, parts))
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
- return changes, location
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
- changes, source = self._adjust_following(index, removed=True)
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': source,
785
- 'path': target})
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', 'path': target, 'value': item})
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
- changes, added = self._adjust_following(index, removed=False)
796
- moved_from = _without_item(source, added)
797
- target = _item_after(added, source, False)
798
- # RFC 6902 does not allow moving a value into its own children
799
- if not _is_inside(target, moved_from):
800
- for op, member, parts in changes:
801
- op[member] = parts
802
- self.remove(index)
803
- if moved_from != target:
804
- self.insert({'op': 'move', 'from': moved_from,
805
- 'path': target})
806
- return
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
- new_index = self.insert({'op': 'remove', 'path': source})
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
- len_src, len_dst = len(src), len(dst)
834
- max_len = max(len_src, len_dst)
835
- min_len = min(len_src, len_dst)
836
- for key in range(max_len):
837
- if key < min_len:
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
- elif len_src > len_dst:
858
- self._item_removed(path, len_dst, src[key])
968
+ for old in src[i1 + paired:i2]:
969
+ self._item_removed(path, j1 + paired, old)
859
970
 
860
- else:
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
- # The DiffBuilder keeps locations as tuples of object keys (str) and array
887
- # indices (int), so it can tell them apart when it adjusts array indices
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 _item_after(location, parts, inserted):
941
- """ Where the item at location is after inserting (or removing, if not
942
- inserted) the value at parts """
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
- RFC 6901 only allows reference tokens to be applied to objects and arrays,
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 is not None and isinstance(subobj, str):
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
- parser = argparse.ArgumentParser(
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
- parser.add_argument('ORIGINAL', type=argparse.FileType('r'),
15
- help='Original file')
16
- parser.add_argument('PATCH', type=argparse.FileType('r'),
17
- nargs='?', default=sys.stdin,
18
- help='Patch file (read from stdin if omitted)')
19
- parser.add_argument('--indent', type=int, default=None,
20
- help='Indent output by n spaces')
21
- parser.add_argument('-b', '--backup', action='store_true',
22
- help='Back up ORIGINAL if modifying in-place')
23
- parser.add_argument('-i', '--in-place', action='store_true',
24
- help='Modify ORIGINAL in-place instead of to stdout')
25
- parser.add_argument('-v', '--version', action='version',
26
- version='%(prog)s ' + jsonpatch.__version__)
27
- parser.add_argument('-u', '--preserve-unicode', action='store_true',
28
- help='Output Unicode character as-is without using Code Point')
29
-
30
- def main():
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
- """ Diffs two JSON files and prints a patch """
39
- args = parser.parse_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()
@@ -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