jsonpatch 1.36__tar.gz → 1.37__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.36/jsonpatch.egg-info → jsonpatch-1.37}/PKG-INFO +1 -1
- {jsonpatch-1.36 → jsonpatch-1.37/jsonpatch.egg-info}/PKG-INFO +1 -1
- {jsonpatch-1.36 → jsonpatch-1.37}/jsonpatch.py +12 -5
- {jsonpatch-1.36 → jsonpatch-1.37}/jsonpatch_cli.py +24 -13
- {jsonpatch-1.36 → jsonpatch-1.37}/property_tests.py +9 -0
- {jsonpatch-1.36 → jsonpatch-1.37}/tests.py +164 -6
- {jsonpatch-1.36 → jsonpatch-1.37}/AUTHORS +0 -0
- {jsonpatch-1.36 → jsonpatch-1.37}/LICENSE +0 -0
- {jsonpatch-1.36 → jsonpatch-1.37}/MANIFEST.in +0 -0
- {jsonpatch-1.36 → jsonpatch-1.37}/README.md +0 -0
- {jsonpatch-1.36 → jsonpatch-1.37}/ext_tests.py +0 -0
- {jsonpatch-1.36 → jsonpatch-1.37}/jsonpatch.egg-info/SOURCES.txt +0 -0
- {jsonpatch-1.36 → jsonpatch-1.37}/jsonpatch.egg-info/dependency_links.txt +0 -0
- {jsonpatch-1.36 → jsonpatch-1.37}/jsonpatch.egg-info/entry_points.txt +0 -0
- {jsonpatch-1.36 → jsonpatch-1.37}/jsonpatch.egg-info/requires.txt +0 -0
- {jsonpatch-1.36 → jsonpatch-1.37}/jsonpatch.egg-info/top_level.txt +0 -0
- {jsonpatch-1.36 → jsonpatch-1.37}/pyproject.toml +0 -0
- {jsonpatch-1.36 → jsonpatch-1.37}/pytest.ini +0 -0
- {jsonpatch-1.36 → jsonpatch-1.37}/requirements.txt +0 -0
- {jsonpatch-1.36 → jsonpatch-1.37}/setup.cfg +0 -0
- {jsonpatch-1.36 → jsonpatch-1.37}/setup.py +0 -0
- {jsonpatch-1.36 → jsonpatch-1.37}/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.37'
|
|
58
58
|
__website__ = 'https://github.com/stefankoegl/python-json-patch'
|
|
59
59
|
__license__ = 'BSD-3-Clause'
|
|
60
60
|
|
|
@@ -384,7 +384,7 @@ class MoveOperation(PatchOperation):
|
|
|
384
384
|
|
|
385
385
|
@property
|
|
386
386
|
def from_path(self):
|
|
387
|
-
from_ptr = self.
|
|
387
|
+
from_ptr = self._from_pointer()
|
|
388
388
|
return '/'.join(from_ptr.parts[:-1])
|
|
389
389
|
|
|
390
390
|
@property
|
|
@@ -396,14 +396,14 @@ class MoveOperation(PatchOperation):
|
|
|
396
396
|
self.set_from_part(-1, value)
|
|
397
397
|
|
|
398
398
|
def get_from_part(self, index):
|
|
399
|
-
from_ptr = self.
|
|
399
|
+
from_ptr = self._from_pointer()
|
|
400
400
|
try:
|
|
401
401
|
return int(from_ptr.parts[index])
|
|
402
402
|
except ValueError:
|
|
403
403
|
return from_ptr.parts[index]
|
|
404
404
|
|
|
405
405
|
def set_from_part(self, index, value):
|
|
406
|
-
from_ptr = self.
|
|
406
|
+
from_ptr = self._from_pointer()
|
|
407
407
|
from_ptr.parts[index] = str(value)
|
|
408
408
|
self.operation['from'] = from_ptr.path
|
|
409
409
|
|
|
@@ -518,11 +518,18 @@ class JsonPatch(object):
|
|
|
518
518
|
self.patch = patch
|
|
519
519
|
self.pointer_cls = pointer_cls
|
|
520
520
|
|
|
521
|
+
if isinstance(patch, (str, bytes, Mapping)):
|
|
522
|
+
raise InvalidJsonPatch("Document is expected to be sequence of operations")
|
|
523
|
+
try:
|
|
524
|
+
patch_iter = iter(patch)
|
|
525
|
+
except TypeError:
|
|
526
|
+
raise InvalidJsonPatch("Document is expected to be sequence of operations")
|
|
527
|
+
|
|
521
528
|
# Verify that the structure of the patch document
|
|
522
529
|
# is correct by retrieving each patch element.
|
|
523
530
|
# Much of the validation is done in the initializer
|
|
524
531
|
# though some is delayed until the patch is applied.
|
|
525
|
-
for op in
|
|
532
|
+
for op in patch_iter:
|
|
526
533
|
# We're only checking for strings in the following check
|
|
527
534
|
# for two reasons:
|
|
528
535
|
#
|
|
@@ -12,8 +12,8 @@ import argparse
|
|
|
12
12
|
|
|
13
13
|
jsondiff_parser = argparse.ArgumentParser(
|
|
14
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'))
|
|
15
|
+
jsondiff_parser.add_argument('FILE1', type=argparse.FileType('r', encoding='utf-8'))
|
|
16
|
+
jsondiff_parser.add_argument('FILE2', type=argparse.FileType('r', encoding='utf-8'))
|
|
17
17
|
jsondiff_parser.add_argument('--indent', type=int, default=None,
|
|
18
18
|
help='Indent output by n spaces')
|
|
19
19
|
jsondiff_parser.add_argument('-u', '--preserve-unicode', action='store_true',
|
|
@@ -43,9 +43,9 @@ def diff_files():
|
|
|
43
43
|
jsonpatch_parser = argparse.ArgumentParser(
|
|
44
44
|
prog='jsonpatch',
|
|
45
45
|
description='Apply a JSON patch on a JSON file')
|
|
46
|
-
jsonpatch_parser.add_argument('ORIGINAL', type=argparse.FileType('r'),
|
|
46
|
+
jsonpatch_parser.add_argument('ORIGINAL', type=argparse.FileType('r', encoding='utf-8'),
|
|
47
47
|
help='Original file')
|
|
48
|
-
jsonpatch_parser.add_argument('PATCH', type=argparse.FileType('r'),
|
|
48
|
+
jsonpatch_parser.add_argument('PATCH', type=argparse.FileType('r', encoding='utf-8'),
|
|
49
49
|
nargs='?', default=sys.stdin,
|
|
50
50
|
help='Patch file (read from stdin if omitted)')
|
|
51
51
|
jsonpatch_parser.add_argument('--indent', type=int, default=None,
|
|
@@ -86,7 +86,7 @@ def patch_files():
|
|
|
86
86
|
# because atomic renames do not work across mount points.)
|
|
87
87
|
|
|
88
88
|
fd, pathname = tempfile.mkstemp(dir=dirname)
|
|
89
|
-
fp = os.fdopen(fd, 'w')
|
|
89
|
+
fp = os.fdopen(fd, 'w', encoding='utf-8')
|
|
90
90
|
atomic = True
|
|
91
91
|
|
|
92
92
|
except OSError:
|
|
@@ -96,7 +96,7 @@ def patch_files():
|
|
|
96
96
|
|
|
97
97
|
if args.backup:
|
|
98
98
|
os.rename(args.ORIGINAL.name, args.ORIGINAL.name + '.orig')
|
|
99
|
-
fp = open(args.ORIGINAL.name, 'w')
|
|
99
|
+
fp = open(args.ORIGINAL.name, 'w', encoding='utf-8')
|
|
100
100
|
atomic = False
|
|
101
101
|
|
|
102
102
|
else:
|
|
@@ -108,15 +108,26 @@ def patch_files():
|
|
|
108
108
|
# By this point we have some sort of file object we can write the
|
|
109
109
|
# modified JSON to.
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
try:
|
|
112
|
+
json.dump(result, fp, indent=args.indent, ensure_ascii=not(args.preserve_unicode))
|
|
113
|
+
fp.write('\n')
|
|
114
|
+
if args.in_place:
|
|
115
|
+
fp.close()
|
|
116
|
+
except BaseException:
|
|
117
|
+
if args.in_place:
|
|
118
|
+
# Cleanup must not replace the original output error.
|
|
119
|
+
try:
|
|
120
|
+
fp.close()
|
|
121
|
+
except OSError:
|
|
122
|
+
pass
|
|
123
|
+
if atomic:
|
|
124
|
+
try:
|
|
125
|
+
os.unlink(pathname)
|
|
126
|
+
except OSError:
|
|
127
|
+
pass
|
|
128
|
+
raise
|
|
113
129
|
|
|
114
130
|
if args.in_place:
|
|
115
|
-
# Close the new file. If we aren't replacing atomically, this
|
|
116
|
-
# is our last step, since everything else is already in place.
|
|
117
|
-
|
|
118
|
-
fp.close()
|
|
119
|
-
|
|
120
131
|
if atomic:
|
|
121
132
|
try:
|
|
122
133
|
# Complete the atomic replace by linking the original
|
|
@@ -324,6 +324,15 @@ class MakePatchProperties(unittest.TestCase):
|
|
|
324
324
|
|
|
325
325
|
class ApplyPatchProperties(unittest.TestCase):
|
|
326
326
|
|
|
327
|
+
@given(st.one_of(json_scalars, st.dictionaries(json_keys, json_docs)))
|
|
328
|
+
@example(patch={})
|
|
329
|
+
@example(patch='')
|
|
330
|
+
def test_non_array_patch_documents_are_rejected(self, patch):
|
|
331
|
+
with self.assertRaises(jsonpatch.InvalidJsonPatch):
|
|
332
|
+
jsonpatch.JsonPatch(patch)
|
|
333
|
+
with self.assertRaises(jsonpatch.InvalidJsonPatch):
|
|
334
|
+
jsonpatch.JsonPatch.from_string(json.dumps(patch))
|
|
335
|
+
|
|
327
336
|
@given(docs_with_patches())
|
|
328
337
|
def test_does_not_modify_document(self, case):
|
|
329
338
|
doc, patch = case
|
|
@@ -1533,6 +1533,35 @@ class JsonPointerTests(unittest.TestCase):
|
|
|
1533
1533
|
|
|
1534
1534
|
class JsonPatchCreationTest(unittest.TestCase):
|
|
1535
1535
|
|
|
1536
|
+
def test_creation_rejects_non_sequence_documents(self):
|
|
1537
|
+
for patch in ({}, {'op': 'add', 'path': '/a', 'value': 1},
|
|
1538
|
+
MappingProxyType({}), '', b'', None, True, False,
|
|
1539
|
+
0, 1, 1.5):
|
|
1540
|
+
with self.subTest(patch=patch):
|
|
1541
|
+
with self.assertRaises(jsonpatch.InvalidJsonPatch):
|
|
1542
|
+
jsonpatch.JsonPatch(patch)
|
|
1543
|
+
|
|
1544
|
+
def test_serialized_patch_rejects_non_array_documents(self):
|
|
1545
|
+
for patch in ({}, {'op': 'add', 'path': '/a', 'value': 1},
|
|
1546
|
+
'', 'text', None, True, False, 0, 1, 1.5):
|
|
1547
|
+
for serialized in (json.dumps(patch), json.dumps(patch).encode()):
|
|
1548
|
+
with self.subTest(patch=serialized):
|
|
1549
|
+
with self.assertRaises(jsonpatch.InvalidJsonPatch):
|
|
1550
|
+
jsonpatch.JsonPatch.from_string(serialized)
|
|
1551
|
+
doc = {'a': 'unchanged'}
|
|
1552
|
+
with self.assertRaises(jsonpatch.InvalidJsonPatch):
|
|
1553
|
+
jsonpatch.apply_patch(doc, serialized, in_place=True)
|
|
1554
|
+
self.assertEqual(doc, {'a': 'unchanged'})
|
|
1555
|
+
|
|
1556
|
+
def test_creation_preserves_operation_sequences(self):
|
|
1557
|
+
for operations in ([], (),
|
|
1558
|
+
[{'op': 'add', 'path': '/a', 'value': 1}],
|
|
1559
|
+
({'op': 'add', 'path': '/a', 'value': 1},)):
|
|
1560
|
+
with self.subTest(operations=operations):
|
|
1561
|
+
patch = jsonpatch.JsonPatch(operations)
|
|
1562
|
+
self.assertIs(patch.patch, operations)
|
|
1563
|
+
self.assertEqual(patch.apply({}), {'a': 1} if operations else {})
|
|
1564
|
+
|
|
1536
1565
|
def test_creation_fails_with_invalid_patch(self):
|
|
1537
1566
|
invalid_patches = [
|
|
1538
1567
|
{ 'path': '/foo', 'value': 'bar'},
|
|
@@ -1676,6 +1705,34 @@ class OperationLocationTests(unittest.TestCase):
|
|
|
1676
1705
|
self.assertEqual(operation, {'op': 'move', 'from': '/c~1d/0', 'path': '/b'})
|
|
1677
1706
|
self.assertEqual(op.apply({'c/d': ['x']}), {'c/d': [], 'b': 'x'})
|
|
1678
1707
|
|
|
1708
|
+
def test_from_path_with_pointer_object(self):
|
|
1709
|
+
for pointer_cls in (jsonpointer.JsonPointer, CustomJsonPointer):
|
|
1710
|
+
with self.subTest(pointer_cls=pointer_cls):
|
|
1711
|
+
op = jsonpatch.MoveOperation(
|
|
1712
|
+
{'op': 'move', 'from': pointer_cls('/a/1/b'), 'path': '/c'},
|
|
1713
|
+
pointer_cls=pointer_cls)
|
|
1714
|
+
self.assertEqual(op.from_path, 'a/1')
|
|
1715
|
+
|
|
1716
|
+
def test_from_parts_with_pointer_object(self):
|
|
1717
|
+
for pointer_cls in (jsonpointer.JsonPointer, CustomJsonPointer):
|
|
1718
|
+
with self.subTest(pointer_cls=pointer_cls):
|
|
1719
|
+
op = jsonpatch.MoveOperation(
|
|
1720
|
+
{'op': 'move', 'from': pointer_cls('/a/1/b'), 'path': '/c'},
|
|
1721
|
+
pointer_cls=pointer_cls)
|
|
1722
|
+
self.assertEqual(op.from_key, 'b')
|
|
1723
|
+
self.assertEqual(op.get_from_part(1), 1)
|
|
1724
|
+
|
|
1725
|
+
def test_set_from_key_with_pointer_object(self):
|
|
1726
|
+
for pointer_cls in (jsonpointer.JsonPointer, CustomJsonPointer):
|
|
1727
|
+
with self.subTest(pointer_cls=pointer_cls):
|
|
1728
|
+
operation = {'op': 'move', 'from': pointer_cls('/a/1'),
|
|
1729
|
+
'path': '/b'}
|
|
1730
|
+
op = jsonpatch.MoveOperation(operation, pointer_cls=pointer_cls)
|
|
1731
|
+
op.from_key = 0
|
|
1732
|
+
self.assertEqual(operation['from'], '/a/0')
|
|
1733
|
+
self.assertEqual(op.apply({'a': ['x', 'y']}),
|
|
1734
|
+
{'a': ['y'], 'b': 'x'})
|
|
1735
|
+
|
|
1679
1736
|
|
|
1680
1737
|
class CustomJsonPointer(jsonpointer.JsonPointer):
|
|
1681
1738
|
pass
|
|
@@ -1836,28 +1893,35 @@ class CommandLineTests(unittest.TestCase):
|
|
|
1836
1893
|
self.addCleanup(tmp.cleanup)
|
|
1837
1894
|
self.dir = tmp.name
|
|
1838
1895
|
|
|
1839
|
-
def write(self, name, value):
|
|
1896
|
+
def write(self, name, value, ensure_ascii=True):
|
|
1840
1897
|
path = os.path.join(self.dir, name)
|
|
1841
1898
|
with open(path, 'w', encoding='utf-8') as f:
|
|
1842
|
-
json.dump(value, f)
|
|
1899
|
+
json.dump(value, f, ensure_ascii=ensure_ascii)
|
|
1843
1900
|
return path
|
|
1844
1901
|
|
|
1845
1902
|
def read(self, path):
|
|
1846
1903
|
with open(path, encoding='utf-8') as f:
|
|
1847
1904
|
return json.load(f)
|
|
1848
1905
|
|
|
1849
|
-
def
|
|
1906
|
+
def run_python(self, code, *args, stdin='', utf8=True):
|
|
1850
1907
|
here = os.path.dirname(os.path.abspath(__file__))
|
|
1851
1908
|
# the scripts have to import this jsonpatch, not an installed one
|
|
1852
1909
|
pythonpath = [here] + [p for p in [os.environ.get('PYTHONPATH')] if p]
|
|
1853
1910
|
env = dict(os.environ, PYTHONPATH=os.pathsep.join(pythonpath),
|
|
1854
1911
|
PYTHONUTF8='1')
|
|
1912
|
+
if not utf8:
|
|
1913
|
+
# Exercise locale-dependent file I/O while keeping pipes in UTF-8.
|
|
1914
|
+
env.update(PYTHONUTF8='0', PYTHONIOENCODING='utf-8',
|
|
1915
|
+
LC_ALL='C', PYTHONCOERCECLOCALE='0')
|
|
1855
1916
|
return subprocess.run(
|
|
1856
|
-
[sys.executable, '-c',
|
|
1857
|
-
'import jsonpatch_cli; jsonpatch_cli.{0}_main()'.format(script)]
|
|
1858
|
-
+ list(args),
|
|
1917
|
+
[sys.executable, '-c', code] + list(args),
|
|
1859
1918
|
input=stdin, capture_output=True, encoding='utf-8', env=env)
|
|
1860
1919
|
|
|
1920
|
+
def run_script(self, script, *args, stdin='', utf8=True):
|
|
1921
|
+
return self.run_python(
|
|
1922
|
+
'import jsonpatch_cli; jsonpatch_cli.{0}_main()'.format(script),
|
|
1923
|
+
*args, stdin=stdin, utf8=utf8)
|
|
1924
|
+
|
|
1861
1925
|
def test_jsondiff(self):
|
|
1862
1926
|
src = self.write('a.json', {'a': [1, 2], 'b': 0})
|
|
1863
1927
|
dst = self.write('b.json', {'a': [1, 2, 3], 'c': 100})
|
|
@@ -1887,6 +1951,18 @@ class CommandLineTests(unittest.TestCase):
|
|
|
1887
1951
|
self.assertIn('"\\u00e4"', self.run_script('jsondiff', src, dst).stdout)
|
|
1888
1952
|
self.assertIn('"ä"', self.run_script('jsondiff', '-u', src, dst).stdout)
|
|
1889
1953
|
|
|
1954
|
+
def test_jsondiff_utf8_files_without_utf8_mode(self):
|
|
1955
|
+
src = self.write('a.json', {'a': 'old', 'unchanged': 'ä'},
|
|
1956
|
+
ensure_ascii=False)
|
|
1957
|
+
dst = self.write('b.json', {'a': '🚀', 'unchanged': 'ä'},
|
|
1958
|
+
ensure_ascii=False)
|
|
1959
|
+
res = self.run_script('jsondiff', src, dst, utf8=False)
|
|
1960
|
+
self.assertEqual(res.returncode, 1, res.stderr)
|
|
1961
|
+
patch = json.loads(res.stdout)
|
|
1962
|
+
self.assertEqual(patch, [{'op': 'replace', 'path': '/a', 'value': '🚀'}])
|
|
1963
|
+
self.assertEqual(jsonpatch.apply_patch(self.read(src), patch),
|
|
1964
|
+
self.read(dst))
|
|
1965
|
+
|
|
1890
1966
|
def test_jsonpatch(self):
|
|
1891
1967
|
doc = self.write('doc.json', {'a': [1, 2], 'b': 0})
|
|
1892
1968
|
patch = self.write('patch.json', [{'op': 'add', 'path': '/a/-', 'value': 3},
|
|
@@ -1903,6 +1979,17 @@ class CommandLineTests(unittest.TestCase):
|
|
|
1903
1979
|
self.assertEqual(res.returncode, 0, res.stderr)
|
|
1904
1980
|
self.assertEqual(json.loads(res.stdout), {'a': 2})
|
|
1905
1981
|
|
|
1982
|
+
def test_jsonpatch_utf8_files_without_utf8_mode(self):
|
|
1983
|
+
doc = self.write('doc.json', {'a': 'ä'}, ensure_ascii=False)
|
|
1984
|
+
patch = self.write('patch.json', [
|
|
1985
|
+
{'op': 'test', 'path': '/a', 'value': 'ä'},
|
|
1986
|
+
{'op': 'replace', 'path': '/a', 'value': '🚀'},
|
|
1987
|
+
], ensure_ascii=False)
|
|
1988
|
+
res = self.run_script('jsonpatch', doc, patch, utf8=False)
|
|
1989
|
+
self.assertEqual(res.returncode, 0, res.stderr)
|
|
1990
|
+
self.assertEqual(json.loads(res.stdout), {'a': '🚀'})
|
|
1991
|
+
self.assertEqual(self.read(doc), {'a': 'ä'})
|
|
1992
|
+
|
|
1906
1993
|
def test_jsonpatch_indent_preserve_unicode(self):
|
|
1907
1994
|
doc = self.write('doc.json', {'a': 'x'})
|
|
1908
1995
|
patch = self.write('patch.json', [{'op': 'replace', 'path': '/a', 'value': 'ä'}])
|
|
@@ -1920,6 +2007,28 @@ class CommandLineTests(unittest.TestCase):
|
|
|
1920
2007
|
# the temporary file has been moved over the original
|
|
1921
2008
|
self.assertEqual(sorted(os.listdir(self.dir)), ['doc.json', 'patch.json'])
|
|
1922
2009
|
|
|
2010
|
+
def test_jsonpatch_in_place_utf8_without_utf8_mode(self):
|
|
2011
|
+
for atomic in (True, False):
|
|
2012
|
+
with self.subTest(atomic=atomic):
|
|
2013
|
+
doc = self.write('doc.json', {'a': 'old'})
|
|
2014
|
+
patch = self.write('patch.json', [
|
|
2015
|
+
{'op': 'replace', 'path': '/a', 'value': '🚀'},
|
|
2016
|
+
])
|
|
2017
|
+
if atomic:
|
|
2018
|
+
code = 'import jsonpatch_cli; jsonpatch_cli.jsonpatch_main()'
|
|
2019
|
+
else:
|
|
2020
|
+
code = (
|
|
2021
|
+
'from unittest import mock\n'
|
|
2022
|
+
'import jsonpatch_cli\n'
|
|
2023
|
+
'with mock.patch("tempfile.mkstemp", side_effect=OSError):\n'
|
|
2024
|
+
' jsonpatch_cli.jsonpatch_main()\n'
|
|
2025
|
+
)
|
|
2026
|
+
res = self.run_python(code, '-i', '-u', doc, patch, utf8=False)
|
|
2027
|
+
self.assertEqual((res.returncode, res.stdout), (0, ''), res.stderr)
|
|
2028
|
+
self.assertEqual(self.read(doc), {'a': '🚀'})
|
|
2029
|
+
self.assertEqual(sorted(os.listdir(self.dir)),
|
|
2030
|
+
['doc.json', 'patch.json'])
|
|
2031
|
+
|
|
1923
2032
|
# 2026-10-11: Windows cannot replace the original while its input is open.
|
|
1924
2033
|
def test_jsonpatch_in_place_closes_original_before_replace(self):
|
|
1925
2034
|
doc = self.write('doc.json', {'a': 1})
|
|
@@ -1956,6 +2065,55 @@ class CommandLineTests(unittest.TestCase):
|
|
|
1956
2065
|
self.assertEqual(self.read(doc), {'a': 2})
|
|
1957
2066
|
self.assertEqual(self.read(doc + '.orig'), {'a': 1})
|
|
1958
2067
|
|
|
2068
|
+
def test_jsonpatch_in_place_cleans_temp_on_output_error(self):
|
|
2069
|
+
for phase in ('dump', 'newline', 'close', 'dump_and_close'):
|
|
2070
|
+
with self.subTest(phase=phase):
|
|
2071
|
+
tmp = tempfile.TemporaryDirectory()
|
|
2072
|
+
self.addCleanup(tmp.cleanup)
|
|
2073
|
+
self.dir = tmp.name
|
|
2074
|
+
doc = self.write('doc.json', {'a': 1})
|
|
2075
|
+
patch = self.write('patch.json', [])
|
|
2076
|
+
args = jsonpatch_cli.jsonpatch_parser.parse_args(['-i', doc, patch])
|
|
2077
|
+
self.addCleanup(args.ORIGINAL.close)
|
|
2078
|
+
self.addCleanup(args.PATCH.close)
|
|
2079
|
+
failure = OSError('output failed')
|
|
2080
|
+
opened = []
|
|
2081
|
+
fdopen = os.fdopen
|
|
2082
|
+
|
|
2083
|
+
def open_output(fd, mode, **kwargs):
|
|
2084
|
+
stream = fdopen(fd, mode, **kwargs)
|
|
2085
|
+
self.addCleanup(stream.close)
|
|
2086
|
+
opened.append(stream)
|
|
2087
|
+
proxy = mock.Mock(wraps=stream)
|
|
2088
|
+
if phase == 'newline':
|
|
2089
|
+
proxy.write.side_effect = lambda value: (
|
|
2090
|
+
stream.write(value) if value != '\n' else fail())
|
|
2091
|
+
if phase in ('close', 'dump_and_close'):
|
|
2092
|
+
def close():
|
|
2093
|
+
stream.close()
|
|
2094
|
+
raise failure if phase == 'close' else OSError('close failed')
|
|
2095
|
+
proxy.close.side_effect = close
|
|
2096
|
+
return proxy
|
|
2097
|
+
|
|
2098
|
+
def fail():
|
|
2099
|
+
raise failure
|
|
2100
|
+
|
|
2101
|
+
def dump(value, stream, **kwargs):
|
|
2102
|
+
stream.write('{')
|
|
2103
|
+
if phase in ('dump', 'dump_and_close'):
|
|
2104
|
+
raise failure
|
|
2105
|
+
|
|
2106
|
+
with mock.patch.object(jsonpatch_cli.jsonpatch_parser, 'parse_args',
|
|
2107
|
+
return_value=args), \
|
|
2108
|
+
mock.patch('os.fdopen', side_effect=open_output), \
|
|
2109
|
+
mock.patch('json.dump', side_effect=dump):
|
|
2110
|
+
with self.assertRaises(OSError) as caught:
|
|
2111
|
+
jsonpatch_cli.patch_files()
|
|
2112
|
+
self.assertIs(caught.exception, failure)
|
|
2113
|
+
self.assertTrue(opened[0].closed)
|
|
2114
|
+
self.assertEqual(self.read(doc), {'a': 1})
|
|
2115
|
+
self.assertEqual(sorted(os.listdir(self.dir)), ['doc.json', 'patch.json'])
|
|
2116
|
+
|
|
1959
2117
|
def test_jsonpatch_in_place_preserves_original_on_replace_error(self):
|
|
1960
2118
|
doc = self.write('doc.json', {'a': 1})
|
|
1961
2119
|
patch = self.write('patch.json', [{'op': 'replace', 'path': '/a', 'value': 2}])
|
|
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
|