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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsonpatch
3
- Version: 1.35
3
+ Version: 1.36
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsonpatch
3
- Version: 1.35
3
+ Version: 1.36
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
@@ -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.35'
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.rename(pathname, args.ORIGINAL.name)
129
+ os.replace(pathname, args.ORIGINAL.name)
130
130
 
131
131
  except OSError:
132
- # In the event we could not actually do the atomic
133
- # replace, unlink the original to move it out of the
134
- # way and finally move the temporary file into place.
135
-
136
- os.unlink(args.ORIGINAL.name)
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 test_jsonpatch_in_place_closes_original_before_rename(self):
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
- rename = os.rename
1931
+ replace = os.replace
1913
1932
 
1914
- def rename_closed_original(source, target):
1933
+ def replace_closed_original(source, target):
1915
1934
  self.assertTrue(args.ORIGINAL.closed)
1916
- return rename(source, target)
1935
+ return replace(source, target)
1917
1936
 
1918
1937
  with mock.patch.object(parser, 'parse_args', return_value=args), \
1919
- mock.patch('os.rename', side_effect=rename_closed_original):
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