jsonpatch 1.35__tar.gz → 1.36__tar.gz
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.35/jsonpatch.egg-info → jsonpatch-1.36}/PKG-INFO +1 -1
- {jsonpatch-1.35 → jsonpatch-1.36/jsonpatch.egg-info}/PKG-INFO +1 -1
- {jsonpatch-1.35 → jsonpatch-1.36}/jsonpatch.py +14 -3
- {jsonpatch-1.35 → jsonpatch-1.36}/jsonpatch_cli.py +6 -7
- {jsonpatch-1.35 → jsonpatch-1.36}/property_tests.py +9 -0
- {jsonpatch-1.35 → jsonpatch-1.36}/tests.py +70 -5
- {jsonpatch-1.35 → jsonpatch-1.36}/AUTHORS +0 -0
- {jsonpatch-1.35 → jsonpatch-1.36}/LICENSE +0 -0
- {jsonpatch-1.35 → jsonpatch-1.36}/MANIFEST.in +0 -0
- {jsonpatch-1.35 → jsonpatch-1.36}/README.md +0 -0
- {jsonpatch-1.35 → jsonpatch-1.36}/ext_tests.py +0 -0
- {jsonpatch-1.35 → jsonpatch-1.36}/jsonpatch.egg-info/SOURCES.txt +0 -0
- {jsonpatch-1.35 → jsonpatch-1.36}/jsonpatch.egg-info/dependency_links.txt +0 -0
- {jsonpatch-1.35 → jsonpatch-1.36}/jsonpatch.egg-info/entry_points.txt +0 -0
- {jsonpatch-1.35 → jsonpatch-1.36}/jsonpatch.egg-info/requires.txt +0 -0
- {jsonpatch-1.35 → jsonpatch-1.36}/jsonpatch.egg-info/top_level.txt +0 -0
- {jsonpatch-1.35 → jsonpatch-1.36}/pyproject.toml +0 -0
- {jsonpatch-1.35 → jsonpatch-1.36}/pytest.ini +0 -0
- {jsonpatch-1.35 → jsonpatch-1.36}/requirements.txt +0 -0
- {jsonpatch-1.35 → jsonpatch-1.36}/setup.cfg +0 -0
- {jsonpatch-1.35 → jsonpatch-1.36}/setup.py +0 -0
- {jsonpatch-1.35 → jsonpatch-1.36}/tests.js +0 -0
|
@@ -54,7 +54,7 @@ _EXACT_MATCH_LIMIT = 500 ** 2
|
|
|
54
54
|
|
|
55
55
|
# Will be parsed by setup.py to determine package metadata
|
|
56
56
|
__author__ = 'Stefan Kögl <stefan@skoegl.net>'
|
|
57
|
-
__version__ = '1.
|
|
57
|
+
__version__ = '1.36'
|
|
58
58
|
__website__ = 'https://github.com/stefankoegl/python-json-patch'
|
|
59
59
|
__license__ = 'BSD-3-Clause'
|
|
60
60
|
|
|
@@ -597,6 +597,10 @@ class JsonPatch(object):
|
|
|
597
597
|
:param dst: Data target document object.
|
|
598
598
|
:type dst: dict
|
|
599
599
|
|
|
600
|
+
:param optimization: Generate move operations for matching values.
|
|
601
|
+
Set to False to use only add, remove and replace.
|
|
602
|
+
:type optimization: bool
|
|
603
|
+
|
|
600
604
|
:param dumps: A function of one argument that produces a serialized
|
|
601
605
|
JSON string.
|
|
602
606
|
:type dumps: Callable
|
|
@@ -614,7 +618,8 @@ class JsonPatch(object):
|
|
|
614
618
|
True
|
|
615
619
|
"""
|
|
616
620
|
json_dumper = dumps or cls.json_dumper
|
|
617
|
-
builder = DiffBuilder(src, dst, json_dumper, pointer_cls=pointer_cls
|
|
621
|
+
builder = DiffBuilder(src, dst, json_dumper, pointer_cls=pointer_cls,
|
|
622
|
+
optimization=optimization)
|
|
618
623
|
builder._compare_values((), None, src, dst)
|
|
619
624
|
ops = list(builder.execute())
|
|
620
625
|
return cls(ops, pointer_cls=pointer_cls)
|
|
@@ -670,9 +675,11 @@ class JsonPatch(object):
|
|
|
670
675
|
|
|
671
676
|
class DiffBuilder(object):
|
|
672
677
|
|
|
673
|
-
def __init__(self, src_doc, dst_doc, dumps=json.dumps, pointer_cls=JsonPointer
|
|
678
|
+
def __init__(self, src_doc, dst_doc, dumps=json.dumps, pointer_cls=JsonPointer,
|
|
679
|
+
optimization=True):
|
|
674
680
|
self.dumps = dumps
|
|
675
681
|
self.pointer_cls = pointer_cls
|
|
682
|
+
self.optimization = optimization
|
|
676
683
|
self.index_storage = [{}, {}]
|
|
677
684
|
self.__root = root = []
|
|
678
685
|
self.src_doc = src_doc
|
|
@@ -693,10 +700,14 @@ class DiffBuilder(object):
|
|
|
693
700
|
return id(value)
|
|
694
701
|
|
|
695
702
|
def store_index(self, value, index, st):
|
|
703
|
+
if not self.optimization:
|
|
704
|
+
return
|
|
696
705
|
storage = self.index_storage[st]
|
|
697
706
|
storage.setdefault(self._move_key(value), []).append(index)
|
|
698
707
|
|
|
699
708
|
def take_index(self, value, st):
|
|
709
|
+
if not self.optimization:
|
|
710
|
+
return None
|
|
700
711
|
stored = self.index_storage[st].get(self._move_key(value))
|
|
701
712
|
if stored:
|
|
702
713
|
return stored.pop()
|
|
@@ -126,12 +126,11 @@ def patch_files():
|
|
|
126
126
|
if args.backup:
|
|
127
127
|
os.link(args.ORIGINAL.name, args.ORIGINAL.name + '.orig')
|
|
128
128
|
os.chmod(pathname, os.stat(args.ORIGINAL.name).st_mode)
|
|
129
|
-
os.
|
|
129
|
+
os.replace(pathname, args.ORIGINAL.name)
|
|
130
130
|
|
|
131
131
|
except OSError:
|
|
132
|
-
#
|
|
133
|
-
# replace
|
|
134
|
-
#
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
os.rename(pathname, args.ORIGINAL.name)
|
|
132
|
+
# Preserve the original if backup, permissions, or replacement
|
|
133
|
+
# fails. replace() can overwrite an existing file on Windows
|
|
134
|
+
# without unlinking it first.
|
|
135
|
+
os.unlink(pathname)
|
|
136
|
+
raise
|
|
@@ -290,6 +290,15 @@ class MakePatchProperties(unittest.TestCase):
|
|
|
290
290
|
def test_roundtrip(self, docs):
|
|
291
291
|
self.check_roundtrip(*docs)
|
|
292
292
|
|
|
293
|
+
@given(doc_pairs)
|
|
294
|
+
def test_roundtrip_without_move_optimization(self, docs):
|
|
295
|
+
src, dst = docs
|
|
296
|
+
patch = jsonpatch.JsonPatch.from_diff(src, dst, optimization=False)
|
|
297
|
+
note(patch.patch)
|
|
298
|
+
self.assertTrue(all(op['op'] in ('add', 'remove', 'replace')
|
|
299
|
+
for op in patch))
|
|
300
|
+
assert_json_equal(patch.apply(src), dst)
|
|
301
|
+
|
|
293
302
|
@given(safe_doc_pairs)
|
|
294
303
|
# the diff considered e.g. 1 and true equal, #180
|
|
295
304
|
@example(docs=([0], [False]))
|
|
@@ -21,6 +21,25 @@ from unittest import mock
|
|
|
21
21
|
|
|
22
22
|
class ApplyPatchTestCase(unittest.TestCase):
|
|
23
23
|
|
|
24
|
+
def test_diff_without_optimization_uses_basic_operations(self):
|
|
25
|
+
documents = [
|
|
26
|
+
({'old': {'value': 1}}, {'new': {'value': 1}}),
|
|
27
|
+
([1, 2, 3], [3, 1, 2]),
|
|
28
|
+
([{'a': 1}, {'b': 2}], [{'b': 2}, {'a': 1}]),
|
|
29
|
+
({'a/b': [1, 2]}, {'~key': [1, 2]}),
|
|
30
|
+
]
|
|
31
|
+
for src, dst in documents:
|
|
32
|
+
with self.subTest(src=src, dst=dst):
|
|
33
|
+
original = copy.deepcopy(src)
|
|
34
|
+
patch = jsonpatch.JsonPatch.from_diff(
|
|
35
|
+
src, dst, optimization=False)
|
|
36
|
+
self.assertTrue(all(operation['op'] in
|
|
37
|
+
('add', 'remove', 'replace')
|
|
38
|
+
for operation in patch))
|
|
39
|
+
self.assertEqual(patch.apply(src), dst)
|
|
40
|
+
self.assertEqual(src, original)
|
|
41
|
+
|
|
42
|
+
|
|
24
43
|
def test_js_file(self):
|
|
25
44
|
with open('./tests.js', 'r') as f:
|
|
26
45
|
tests = json.load(f)
|
|
@@ -1902,21 +1921,21 @@ class CommandLineTests(unittest.TestCase):
|
|
|
1902
1921
|
self.assertEqual(sorted(os.listdir(self.dir)), ['doc.json', 'patch.json'])
|
|
1903
1922
|
|
|
1904
1923
|
# 2026-10-11: Windows cannot replace the original while its input is open.
|
|
1905
|
-
def
|
|
1924
|
+
def test_jsonpatch_in_place_closes_original_before_replace(self):
|
|
1906
1925
|
doc = self.write('doc.json', {'a': 1})
|
|
1907
1926
|
patch = self.write('patch.json', [{'op': 'replace', 'path': '/a', 'value': 2}])
|
|
1908
1927
|
parser = jsonpatch_cli.jsonpatch_parser
|
|
1909
1928
|
args = parser.parse_args(['-i', doc, patch])
|
|
1910
1929
|
self.addCleanup(args.ORIGINAL.close)
|
|
1911
1930
|
self.addCleanup(args.PATCH.close)
|
|
1912
|
-
|
|
1931
|
+
replace = os.replace
|
|
1913
1932
|
|
|
1914
|
-
def
|
|
1933
|
+
def replace_closed_original(source, target):
|
|
1915
1934
|
self.assertTrue(args.ORIGINAL.closed)
|
|
1916
|
-
return
|
|
1935
|
+
return replace(source, target)
|
|
1917
1936
|
|
|
1918
1937
|
with mock.patch.object(parser, 'parse_args', return_value=args), \
|
|
1919
|
-
mock.patch('os.
|
|
1938
|
+
mock.patch('os.replace', side_effect=replace_closed_original):
|
|
1920
1939
|
jsonpatch_cli.patch_files()
|
|
1921
1940
|
self.assertEqual(self.read(doc), {'a': 2})
|
|
1922
1941
|
|
|
@@ -1937,6 +1956,52 @@ class CommandLineTests(unittest.TestCase):
|
|
|
1937
1956
|
self.assertEqual(self.read(doc), {'a': 2})
|
|
1938
1957
|
self.assertEqual(self.read(doc + '.orig'), {'a': 1})
|
|
1939
1958
|
|
|
1959
|
+
def test_jsonpatch_in_place_preserves_original_on_replace_error(self):
|
|
1960
|
+
doc = self.write('doc.json', {'a': 1})
|
|
1961
|
+
patch = self.write('patch.json', [{'op': 'replace', 'path': '/a', 'value': 2}])
|
|
1962
|
+
parser = jsonpatch_cli.jsonpatch_parser
|
|
1963
|
+
args = parser.parse_args(['-i', doc, patch])
|
|
1964
|
+
self.addCleanup(args.ORIGINAL.close)
|
|
1965
|
+
self.addCleanup(args.PATCH.close)
|
|
1966
|
+
|
|
1967
|
+
with mock.patch.object(parser, 'parse_args', return_value=args), \
|
|
1968
|
+
mock.patch('os.rename', side_effect=PermissionError('cannot replace')), \
|
|
1969
|
+
mock.patch('os.replace', side_effect=PermissionError('cannot replace')):
|
|
1970
|
+
with self.assertRaises(PermissionError):
|
|
1971
|
+
jsonpatch_cli.patch_files()
|
|
1972
|
+
|
|
1973
|
+
self.assertTrue(os.path.exists(doc))
|
|
1974
|
+
self.assertEqual(self.read(doc), {'a': 1})
|
|
1975
|
+
self.assertEqual(sorted(os.listdir(self.dir)), ['doc.json', 'patch.json'])
|
|
1976
|
+
|
|
1977
|
+
def test_jsonpatch_in_place_preserves_original_on_chmod_error(self):
|
|
1978
|
+
doc = self.write('doc.json', {'a': 1})
|
|
1979
|
+
patch = self.write('patch.json', [{'op': 'replace', 'path': '/a', 'value': 2}])
|
|
1980
|
+
parser = jsonpatch_cli.jsonpatch_parser
|
|
1981
|
+
args = parser.parse_args(['-i', doc, patch])
|
|
1982
|
+
self.addCleanup(args.ORIGINAL.close)
|
|
1983
|
+
self.addCleanup(args.PATCH.close)
|
|
1984
|
+
|
|
1985
|
+
with mock.patch.object(parser, 'parse_args', return_value=args), \
|
|
1986
|
+
mock.patch('os.chmod', side_effect=PermissionError('cannot chmod')):
|
|
1987
|
+
with self.assertRaises(PermissionError):
|
|
1988
|
+
jsonpatch_cli.patch_files()
|
|
1989
|
+
|
|
1990
|
+
self.assertEqual(self.read(doc), {'a': 1})
|
|
1991
|
+
self.assertEqual(sorted(os.listdir(self.dir)), ['doc.json', 'patch.json'])
|
|
1992
|
+
|
|
1993
|
+
def test_jsonpatch_in_place_existing_backup(self):
|
|
1994
|
+
doc = self.write('doc.json', {'a': 1})
|
|
1995
|
+
patch = self.write('patch.json', [{'op': 'replace', 'path': '/a', 'value': 2}])
|
|
1996
|
+
backup = self.write('doc.json.orig', {'previous': True})
|
|
1997
|
+
res = self.run_script('jsonpatch', '-i', '-b', doc, patch)
|
|
1998
|
+
|
|
1999
|
+
self.assertNotEqual(res.returncode, 0)
|
|
2000
|
+
self.assertEqual(self.read(doc), {'a': 1})
|
|
2001
|
+
self.assertEqual(self.read(backup), {'previous': True})
|
|
2002
|
+
self.assertEqual(sorted(os.listdir(self.dir)),
|
|
2003
|
+
['doc.json', 'doc.json.orig', 'patch.json'])
|
|
2004
|
+
|
|
1940
2005
|
def test_jsonpatch_in_place_failing_patch(self):
|
|
1941
2006
|
# the patch is applied before the file is replaced, so it is kept
|
|
1942
2007
|
doc = self.write('doc.json', {'a': 1})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|