From 50a5e0d7e36bc9aaa8f863ca28fc59f147de7949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Thu, 23 Apr 2015 22:08:06 +0200 Subject: [PATCH 01/39] Enable container-based builds on Travis-CI --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 94f1e7b..f1d2eb9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ python: - "2.7" - "3.2" - "3.3" - - "3.4" + - "3.4" - "pypy" - "pypy3" @@ -17,3 +17,5 @@ script: after_script: - coveralls + +sudo: false From 5d1980dec71a51f61dd1629b283af1f6b2b7c47b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Thu, 7 May 2015 18:08:37 +0200 Subject: [PATCH 02/39] Fix make_patch with unicode paths --- requirements.txt | 2 +- tests.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7b26233..cd4b892 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -jsonpointer>=1.5 +jsonpointer>=1.9 diff --git a/tests.py b/tests.py index 5b0d9e9..071aee0 100755 --- a/tests.py +++ b/tests.py @@ -351,6 +351,14 @@ def test_root_list(self): res = patch.apply(src) self.assertEqual(res, dst) + def test_make_patch_unicode(self): + """ Test if unicode keys and values are handled correctly """ + src = {} + dst = {u'\xee': u'\xee'} + patch = jsonpatch.make_patch(src, dst) + res = patch.apply(src) + self.assertEqual(res, dst) + class InvalidInputTests(unittest.TestCase): From b8275b58ecea23f3cca3cb5dae3a6e0d65f7a541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Thu, 7 May 2015 18:09:03 +0200 Subject: [PATCH 03/39] Require pypandoc instead of pandoc for setup.py --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index fd0fd6c..21daf9a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,2 @@ wheel -pandoc==1.0.0-alpha.3 +pypandoc From 348ec1d9edf543f9c6cd61b780bb6ae7c2200bf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Thu, 7 May 2015 18:10:41 +0200 Subject: [PATCH 04/39] bump version to 1.10 --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index 6318821..6cc972f 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -51,7 +51,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.9' +__version__ = '1.10' __website__ = 'https://github.com/stefankoegl/python-json-patch' __license__ = 'Modified BSD License' From 23f9b1a34ba50687e0e5afbcd9f26817457d5a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Thu, 7 May 2015 18:12:48 +0200 Subject: [PATCH 05/39] Fix tests for Python 3.2 --- tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests.py b/tests.py index 071aee0..5cbff33 100755 --- a/tests.py +++ b/tests.py @@ -354,7 +354,7 @@ def test_root_list(self): def test_make_patch_unicode(self): """ Test if unicode keys and values are handled correctly """ src = {} - dst = {u'\xee': u'\xee'} + dst = {'\xee': '\xee'} patch = jsonpatch.make_patch(src, dst) res = patch.apply(src) self.assertEqual(res, dst) From dc9260e5131c5e30bba951bc8c009cd843be32a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Fri, 8 May 2015 18:07:36 +0200 Subject: [PATCH 06/39] bump version to 1.11 --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index 6cc972f..f62aa94 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -51,7 +51,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.10' +__version__ = '1.11' __website__ = 'https://github.com/stefankoegl/python-json-patch' __license__ = 'Modified BSD License' From 5ee8e9e3fd94647236e75436da6d917f712cc087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Fri, 18 Sep 2015 16:27:39 +0200 Subject: [PATCH 07/39] Add Travis tests for Python 3.5 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index f1d2eb9..e4a40ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - "3.2" - "3.3" - "3.4" + - "3.5" - "pypy" - "pypy3" From 70306faa2bab12ad33aae0b6d515e83170cb3608 Mon Sep 17 00:00:00 2001 From: selurvedu Date: Sun, 18 Oct 2015 14:12:54 +0000 Subject: [PATCH 08/39] Fix build failure on Travis CI with Python 3.2 See https://bitbucket.org/ned/coveragepy/issues/407 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index e4a40ad..70f48e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ python: install: - travis_retry pip install -r requirements.txt + - if [ "$TRAVIS_PYTHON_VERSION" == "3.2" ]; then travis_retry pip install 'coverage<4'; fi - travis_retry pip install coveralls script: From 0ae80ba696ddf8dbbe23c371d30bdc8a27290720 Mon Sep 17 00:00:00 2001 From: selurvedu Date: Fri, 14 Aug 2015 15:37:24 +0000 Subject: [PATCH 09/39] Allow running certain tests separately E.g. `python2 -m unittest tests.MakePatchTestCase.test_objects` or `nose tests:MakePatchTestCase.test_objects`. --- tests.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests.py b/tests.py index 5cbff33..24f3908 100755 --- a/tests.py +++ b/tests.py @@ -414,30 +414,30 @@ def test_replace_missing(self): self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, src, patch_obj) +if __name__ == '__main__': + modules = ['jsonpatch'] -modules = ['jsonpatch'] + def get_suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(jsonpatch)) + suite.addTest(unittest.makeSuite(ApplyPatchTestCase)) + suite.addTest(unittest.makeSuite(EqualityTestCase)) + suite.addTest(unittest.makeSuite(MakePatchTestCase)) + suite.addTest(unittest.makeSuite(InvalidInputTests)) + suite.addTest(unittest.makeSuite(ConflictTests)) + return suite -def get_suite(): - suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(jsonpatch)) - suite.addTest(unittest.makeSuite(ApplyPatchTestCase)) - suite.addTest(unittest.makeSuite(EqualityTestCase)) - suite.addTest(unittest.makeSuite(MakePatchTestCase)) - suite.addTest(unittest.makeSuite(InvalidInputTests)) - suite.addTest(unittest.makeSuite(ConflictTests)) - return suite + suite = get_suite() -suite = get_suite() + for module in modules: + m = __import__(module, fromlist=[module]) + suite.addTest(doctest.DocTestSuite(m)) -for module in modules: - m = __import__(module, fromlist=[module]) - suite.addTest(doctest.DocTestSuite(m)) + runner = unittest.TextTestRunner(verbosity=1) -runner = unittest.TextTestRunner(verbosity=1) + result = runner.run(suite) -result = runner.run(suite) - -if not result.wasSuccessful(): - sys.exit(1) + if not result.wasSuccessful(): + sys.exit(1) From 3f2328eb63507fe78a48c27cf677a6998f3c9f63 Mon Sep 17 00:00:00 2001 From: Alex Pinkney Date: Tue, 3 Nov 2015 17:04:12 +0000 Subject: [PATCH 10/39] Fix bug in _split_by_common_seq using wrong range in right subtree --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index f62aa94..4ffd50f 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -641,7 +641,7 @@ def _split_by_common_seq(src, dst, bx=(0, -1), by=(0, -1)): (by[0], by[0] + y[0])), _split_by_common_seq(src[x[1]:], dst[y[1]:], (bx[0] + x[1], bx[0] + len(src)), - (bx[0] + y[1], bx[0] + len(dst)))] + (by[0] + y[1], by[0] + len(dst)))] def _compare(path, src, dst, left, right): From 2a02d21d7bf7e017376b3e6f08d38e71343f6b98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Tue, 3 Nov 2015 20:00:48 +0100 Subject: [PATCH 11/39] Add failing test for #40 --- tests.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests.py b/tests.py index 24f3908..2572dc0 100755 --- a/tests.py +++ b/tests.py @@ -359,6 +359,13 @@ def test_make_patch_unicode(self): res = patch.apply(src) self.assertEqual(res, dst) + def test_issue40(self): + """ Tests an issue in _split_by_common_seq reported in #40 """ + + src = [8, 7, 2, 1, 0, 9, 4, 3, 5, 6] + dest = [7, 2, 1, 0, 9, 4, 3, 6, 5, 8] + patch = jsonpatch.make_patch(src, dest) + class InvalidInputTests(unittest.TestCase): From d877f1d95dfe878fd06da9b7be8ee0940185b7c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Tue, 3 Nov 2015 20:14:30 +0100 Subject: [PATCH 12/39] bump version to 1.12 --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index 4ffd50f..fb2b90d 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -51,7 +51,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.11' +__version__ = '1.12' __website__ = 'https://github.com/stefankoegl/python-json-patch' __license__ = 'Modified BSD License' From 5c2a9b91897e7b154d857331552a9543d5b0b020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sat, 13 Feb 2016 14:46:53 +0100 Subject: [PATCH 13/39] Add encoding info to setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index d31b990..6b7cfc2 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- import sys import io From 32dcbb03d8c6b9aedefff026fda75e5d8b63b8d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sat, 13 Feb 2016 14:49:05 +0100 Subject: [PATCH 14/39] Create "universal" wheel packages --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2a9acf1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 From a33021bf5a87350abc225a15c2a12880d88ed383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sat, 13 Feb 2016 15:40:03 +0100 Subject: [PATCH 15/39] Optimize "deep" ``replace`` operation, fixes #36 --- jsonpatch.py | 13 ++++++++++--- tests.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/jsonpatch.py b/jsonpatch.py index fb2b90d..838d66c 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -756,11 +756,18 @@ def _optimize(operations): def _optimize_using_replace(prev, cur): - """Optimises JSON patch by using ``replace`` operation instead of - ``remove`` and ``add`` against the same path.""" + """Optimises by replacing ``add``/``remove`` with ``replace`` on same path + + For nested strucures, tries to recurse replacement, see #36 """ prev['op'] = 'replace' if cur['op'] == 'add': - prev['value'] = cur['value'] + # make recursive patch + patch = make_patch(prev['value'], cur['value']) + if len(patch.patch) == 1: + prev['path'] = prev['path'] + patch.patch[0]['path'] + prev['value'] = patch.patch[0]['value'] + else: + prev['value'] = cur['value'] def _optimize_using_move(prev_item, item): diff --git a/tests.py b/tests.py index 2572dc0..b73b38e 100755 --- a/tests.py +++ b/tests.py @@ -366,6 +366,24 @@ def test_issue40(self): dest = [7, 2, 1, 0, 9, 4, 3, 6, 5, 8] patch = jsonpatch.make_patch(src, dest) + def test_minimal_patch(self): + """ Test whether a minimal patch is created, see #36 """ + src = [{"foo": 1, "bar": 2}] + dst = [{"foo": 2, "bar": 2}] + import pudb + #pudb.set_trace() + patch = jsonpatch.make_patch(src, dst) + + exp = [ + { + "path": "/0/foo", + "value": 2, + "op": "replace" + } + ] + + self.assertEqual(patch.patch, exp) + class InvalidInputTests(unittest.TestCase): From 4443d3241b8e11e691d4700b37db469120993d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sat, 13 Feb 2016 15:41:18 +0100 Subject: [PATCH 16/39] Make ``move`` operation with from == path a no-op --- jsonpatch.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jsonpatch.py b/jsonpatch.py index 838d66c..917fc33 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -484,6 +484,10 @@ def apply(self, obj): except (KeyError, IndexError) as ex: raise JsonPatchConflict(str(ex)) + # If source and target are equal, this is a no-op + if self.pointer == from_ptr: + return obj + if isinstance(subobj, MutableMapping) and \ self.pointer.contains(from_ptr): raise JsonPatchConflict('Cannot move values into its own children') From cf0da04dca7a5fcb3f20ab6a4eb78faf9dac8ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sat, 13 Feb 2016 15:42:27 +0100 Subject: [PATCH 17/39] Print test comments when external tests fail Makes it easier to locate the failing tests in the test file --- ext_tests.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ext_tests.py b/ext_tests.py index 5e5ded8..0e1404c 100755 --- a/ext_tests.py +++ b/ext_tests.py @@ -60,12 +60,15 @@ def _test(self, test): ) else: - res = jsonpatch.apply_patch(test['doc'], test['patch']) + try: + res = jsonpatch.apply_patch(test['doc'], test['patch']) + except jsonpatch.JsonPatchException as jpe: + raise Exception(test.get('comment', '')) from jpe # if there is no 'expected' we only verify that applying the patch # does not raies an exception if 'expected' in test: - self.assertEquals(res, test['expected']) + self.assertEquals(res, test['expected'], test.get('comment', '')) def make_test_case(tests): From 9ce8487179bc001c4be10a54dadc04ce66be5c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sat, 13 Feb 2016 15:50:48 +0100 Subject: [PATCH 18/39] Remove import of ``pudb`` --- tests.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests.py b/tests.py index b73b38e..8b0b52c 100755 --- a/tests.py +++ b/tests.py @@ -370,8 +370,6 @@ def test_minimal_patch(self): """ Test whether a minimal patch is created, see #36 """ src = [{"foo": 1, "bar": 2}] dst = [{"foo": 2, "bar": 2}] - import pudb - #pudb.set_trace() patch = jsonpatch.make_patch(src, dst) exp = [ From b15d8f1ec18e4f3191ba668e63520018a47e37de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sat, 13 Feb 2016 19:00:29 +0100 Subject: [PATCH 19/39] bump version to 1.13 --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index 917fc33..96fe6f9 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -51,7 +51,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.12' +__version__ = '1.13' __website__ = 'https://github.com/stefankoegl/python-json-patch' __license__ = 'Modified BSD License' From 65dc01ca6d3350259db18fd2ca47903456815e8f Mon Sep 17 00:00:00 2001 From: selurvedu Date: Wed, 14 Oct 2015 07:55:04 +0000 Subject: [PATCH 20/39] Import jsondiff.py from nxofsys/jsondiff@96e82b Source: https://github.com/nxsofsys/jsondiff/tree/96e82b --- jsondiff.py | 282 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 jsondiff.py diff --git a/jsondiff.py b/jsondiff.py new file mode 100644 index 0000000..5abb755 --- /dev/null +++ b/jsondiff.py @@ -0,0 +1,282 @@ +''' +The MIT License (MIT) + +Copyright (c) 2014 Ilya Volkov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +''' + + +__all__ = ["make",] + +def _store_index(a, x, v): + lo = 0 + hi = len(a) + while lo < hi: + mid = (lo+hi)//2 + if a[mid][0] < x: lo = mid+1 + else: hi = mid + if lo < len(a) and a[lo][0] == x: + a[lo][1].append(v) + else: + a.insert(lo, (x, [v])) + +def _take_index(a, x): + lo = 0 + hi = len(a) + while lo < hi: + mid = (lo+hi)//2 + if a[mid][0] < x: lo = mid+1 + else: hi = mid + if lo < len(a) and a[lo][0] == x: + if a[lo][1]: + return a[lo][1].pop() + return None + +class _compare_info(object): + def __init__(self): + self.removed = [] + self.added = [] + self.__root = root = [] + root[:] = [root, root, None] + + def insert(self, op): + root = self.__root + last = root[0] + last[1] = root[0] = [last, root, op] + return root[0] + + def remove(self, index): + link_prev, link_next, _ = index + link_prev[1] = link_next + link_next[0] = link_prev + index[:] = [] + + def iter_from(self, start): + root = self.__root + curr = start[1] + while curr is not root: + yield curr[2] + curr = curr[1] + + def __iter__(self): + root = self.__root + curr = root[1] + while curr is not root: + yield curr[2] + curr = curr[1] + + def execute(self): + root = self.__root + curr = root[1] + while curr is not root: + if curr[1] is not root: + op_first, op_second = curr[2], curr[1][2] + if op_first.key == op_second.key and \ + op_first.path == op_second.path and \ + type(op_first) == _op_remove and \ + type(op_second) == _op_add: + yield _op_replace(op_second.path, op_second.key, op_second.value).get() + curr = curr[1][1] + continue + yield curr[2].get() + curr = curr[1] + +class _op_base(object): + def __init__(self, path, key, value): + self.path = path + self.key = key + self.value = value + + def __repr__(self): + return str(self.get()) + +class _op_add(_op_base): + def _on_undo_remove(self, path, key): + if self.path == path: + if self.key > key: + self.key += 1 + else: + key += 1 + return key + + def _on_undo_add(self, path, key): + if self.path == path: + if self.key > key: + self.key -= 1 + else: + key += 1 + return key + + def get(self): + return {'op': 'add', 'path': _path_join(self.path, self.key), 'value': self.value} + +class _op_remove(_op_base): + def _on_undo_remove(self, path, key): + if self.path == path: + if self.key >= key: + self.key += 1 + else: + key -= 1 + return key + + def _on_undo_add(self, path, key): + if self.path == path: + if self.key > key: + self.key -= 1 + else: + key -= 1 + return key + + def get(self): + return {'op': 'remove', 'path': _path_join(self.path, self.key)} + +class _op_replace(_op_base): + def _on_undo_remove(self, path, key): + return key + + def _on_undo_add(self, path, key): + return key + + def get(self): + return {'op': 'replace', 'path': _path_join(self.path, self.key), 'value': self.value} + +class _op_move(object): + def __init__(self, oldpath, oldkey, path, key): + self.oldpath = oldpath + self.oldkey = oldkey + self.path = path + self.key = key + + def _on_undo_remove(self, path, key): + if self.oldpath == path: + if self.oldkey >= key: + self.oldkey += 1 + else: + key -= 1 + if self.path == path: + if self.key > key: + self.key += 1 + else: + key += 1 + return key + + def _on_undo_add(self, path, key): + if self.oldpath == path: + if self.oldkey > key: + self.oldkey -= 1 + else: + key -= 1 + if self.path == path: + if self.key > key: + self.key -= 1 + else: + key += 1 + return key + + def get(self): + return {'op': 'move', 'path': _path_join(self.path, self.key), 'from': _path_join(self.oldpath, self.oldkey)} + + def __repr__(self): + return str(self.get()) + +def _path_join(path, key): + if key != None: + return path + '/' + str(key).replace('~', '~0').replace('/', '~1') + return path + +def _item_added(path, key, info, item): + index = _take_index(info.removed, item) + if index != None: + op = index[2] + if type(op.key) == int: + for v in info.iter_from(index): + op.key = v._on_undo_remove(op.path, op.key) + info.remove(index) + if op.path != path or op.key != key: + new_op = _op_move(op.path, op.key, path, key) + info.insert(new_op) + else: + new_op = _op_add(path, key, item) + new_index = info.insert(new_op) + _store_index(info.added, item, new_index) + +def _item_removed(path, key, info, item): + new_op = _op_remove(path, key, item) + index = _take_index(info.added, item) + new_index = info.insert(new_op) + if index != None: + op = index[2] + if type(op.key) == int: + for v in info.iter_from(index): + op.key = v._on_undo_add(op.path, op.key) + info.remove(index) + if new_op.path != op.path or new_op.key != op.key: + new_op = _op_move(new_op.path, new_op.key, op.path, op.key) + new_index[2] = new_op + else: + info.remove(new_index) + else: + _store_index(info.removed, item, new_index) + +def _item_replaced(path, key, info, item): + info.insert(_op_replace(path, key, item)) + +def _compare_dicts(path, info, src, dst): + added_keys = dst.viewkeys() - src.viewkeys() + removed_keys = src.viewkeys() - dst.viewkeys() + for key in removed_keys: + _item_removed(path, str(key), info, src[key]) + for key in added_keys: + _item_added(path, str(key), info, dst[key]) + for key in src.viewkeys() & dst.viewkeys(): + _compare_values(path, key, info, src[key], dst[key]) + +def _compare_lists(path, info, src, dst): + len_src, len_dst = len(src), len(dst) + max_len = max(len_src, len_dst) + min_len = min(len_src, len_dst) + for key in xrange(max_len): + if key < min_len: + old, new = src[key], dst[key] + if old == new: + continue + _item_removed(path, key, info, old) + _item_added(path, key, info, new) + elif len_src > len_dst: + _item_removed(path, len_dst, info, src[key]) + else: + _item_added(path, key, info, dst[key]) + +def _compare_values(path, key, info, src, dst): + if src == dst: + return + elif isinstance(src, dict) and \ + isinstance(dst, dict): + _compare_dicts(_path_join(path, key), info, src, dst) + elif isinstance(src, list) and \ + isinstance(dst, list): + _compare_lists(_path_join(path, key), info, src, dst) + else: + _item_replaced(path, key, info, dst) + +def make(src, dst, **kwargs): + info = _compare_info() + _compare_values('', None, info, src, dst) + return [op for op in info.execute()] From eeb6df0399050b47028fcd1919473a8a63c30c36 Mon Sep 17 00:00:00 2001 From: selurvedu Date: Wed, 14 Oct 2015 08:00:18 +0000 Subject: [PATCH 21/39] Generate patches with jsondiff.py jsondiff.py can only generate the actual patch. jsonpatch.py is still used for everything else. --- jsonpatch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index 6318821..efa384b 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -42,6 +42,8 @@ import json import sys +import jsondiff + try: from collections.abc import MutableMapping, MutableSequence except ImportError: @@ -168,7 +170,7 @@ def make_patch(src, dst): >>> new == dst True """ - return JsonPatch.from_diff(src, dst) + return JsonPatch(jsondiff.make(src, dst)) class JsonPatch(object): From e3e5e0be8c5ea76a508339089a8c315b7d726b9c Mon Sep 17 00:00:00 2001 From: selurvedu Date: Fri, 8 Apr 2016 03:10:53 +0000 Subject: [PATCH 22/39] Add jsondiff into setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index d31b990..44a8f91 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,7 @@ MODULES = ( 'jsonpatch', + 'jsondiff', ) REQUIREMENTS = list(open('requirements.txt')) From 0a0f1b25dc07df616c85f252798482225c7da8b7 Mon Sep 17 00:00:00 2001 From: selurvedu Date: Fri, 8 Apr 2016 02:04:01 +0000 Subject: [PATCH 23/39] Tag: v1.9+jsondiff.0 --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index efa384b..7ec9bf5 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -53,7 +53,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.9' +__version__ = '1.9+jsondiff.0' __website__ = 'https://github.com/stefankoegl/python-json-patch' __license__ = 'Modified BSD License' From 315823c521ef498fbec48f43c0f4c36a84fc47a6 Mon Sep 17 00:00:00 2001 From: selurvedu Date: Thu, 15 Oct 2015 07:09:45 +0000 Subject: [PATCH 24/39] Update to upstream rev.: nxsofsys/jsondiff@7cb4ef9 --- jsondiff.py | 87 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/jsondiff.py b/jsondiff.py index 5abb755..d390312 100644 --- a/jsondiff.py +++ b/jsondiff.py @@ -22,40 +22,53 @@ SOFTWARE. ''' +import sys -__all__ = ["make",] - -def _store_index(a, x, v): - lo = 0 - hi = len(a) - while lo < hi: - mid = (lo+hi)//2 - if a[mid][0] < x: lo = mid+1 - else: hi = mid - if lo < len(a) and a[lo][0] == x: - a[lo][1].append(v) +__all__ = ["make",] + +if sys.version_info[0] >= 3: + _range = range + _viewkeys = dict.keys +else: + _range = xrange + if sys.version_info[1] >= 7: + _viewkeys = dict.viewkeys else: - a.insert(lo, (x, [v])) - -def _take_index(a, x): - lo = 0 - hi = len(a) - while lo < hi: - mid = (lo+hi)//2 - if a[mid][0] < x: lo = mid+1 - else: hi = mid - if lo < len(a) and a[lo][0] == x: - if a[lo][1]: - return a[lo][1].pop() - return None + _viewkeys = lambda x: set(dict.keys(x)) + +_ST_ADD = 0 +_ST_REMOVE = 1 class _compare_info(object): + def __init__(self): - self.removed = [] - self.added = [] + self.index_storage = [{}, {}] + self.index_storage2 = [[], []] self.__root = root = [] root[:] = [root, root, None] + def store_index(self, value, index, st): + try: + storage = self.index_storage[st] + stored = storage.get(value) + if stored == None: + storage[value] = [index] + else: + storage[value].append(index) + except TypeError: + self.index_storage2[st].append((value, index)) + + def take_index(self, value, st): + try: + stored = self.index_storage[st].get(value) + if stored: + return stored.pop() + except TypeError: + storage = self.index_storage2[st] + for i in range(len(storage)-1, -1, -1): + if storage[i][0] == value: + return storage.pop(i)[1] + def insert(self, op): root = self.__root last = root[0] @@ -96,7 +109,7 @@ def execute(self): curr = curr[1][1] continue yield curr[2].get() - curr = curr[1] + curr = curr[1] class _op_base(object): def __init__(self, path, key, value): @@ -192,7 +205,7 @@ def _on_undo_add(self, path, key): def get(self): return {'op': 'move', 'path': _path_join(self.path, self.key), 'from': _path_join(self.oldpath, self.oldkey)} - + def __repr__(self): return str(self.get()) @@ -202,7 +215,7 @@ def _path_join(path, key): return path def _item_added(path, key, info, item): - index = _take_index(info.removed, item) + index = info.take_index(item, _ST_REMOVE) if index != None: op = index[2] if type(op.key) == int: @@ -215,11 +228,11 @@ def _item_added(path, key, info, item): else: new_op = _op_add(path, key, item) new_index = info.insert(new_op) - _store_index(info.added, item, new_index) + info.store_index(item, new_index, _ST_ADD) def _item_removed(path, key, info, item): new_op = _op_remove(path, key, item) - index = _take_index(info.added, item) + index = info.take_index(item, _ST_ADD) new_index = info.insert(new_op) if index != None: op = index[2] @@ -233,26 +246,28 @@ def _item_removed(path, key, info, item): else: info.remove(new_index) else: - _store_index(info.removed, item, new_index) + info.store_index(item, new_index, _ST_REMOVE) def _item_replaced(path, key, info, item): info.insert(_op_replace(path, key, item)) def _compare_dicts(path, info, src, dst): - added_keys = dst.viewkeys() - src.viewkeys() - removed_keys = src.viewkeys() - dst.viewkeys() + src_keys = _viewkeys(src) + dst_keys = _viewkeys(dst) + added_keys = dst_keys - src_keys + removed_keys = src_keys - dst_keys for key in removed_keys: _item_removed(path, str(key), info, src[key]) for key in added_keys: _item_added(path, str(key), info, dst[key]) - for key in src.viewkeys() & dst.viewkeys(): + for key in src_keys & dst_keys: _compare_values(path, key, info, src[key], dst[key]) def _compare_lists(path, info, src, dst): len_src, len_dst = len(src), len(dst) max_len = max(len_src, len_dst) min_len = min(len_src, len_dst) - for key in xrange(max_len): + for key in _range(max_len): if key < min_len: old, new = src[key], dst[key] if old == new: From 3296da38613cd3b1ba34132f401808f0556bf544 Mon Sep 17 00:00:00 2001 From: selurvedu Date: Fri, 8 Apr 2016 02:06:26 +0000 Subject: [PATCH 25/39] Tag: v1.9+jsondiff.1 --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index 7ec9bf5..c559a55 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -53,7 +53,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.9+jsondiff.0' +__version__ = '1.9+jsondiff.1' __website__ = 'https://github.com/stefankoegl/python-json-patch' __license__ = 'Modified BSD License' From a059d06b086b700ee76a46f7b0f0cbbf3f6f941b Mon Sep 17 00:00:00 2001 From: selurvedu Date: Fri, 8 Apr 2016 01:09:40 +0000 Subject: [PATCH 26/39] Use unicode() instead of str() on Python 2 --- jsondiff.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/jsondiff.py b/jsondiff.py index d390312..bfa60af 100644 --- a/jsondiff.py +++ b/jsondiff.py @@ -28,9 +28,11 @@ if sys.version_info[0] >= 3: _range = range + _str = str _viewkeys = dict.keys else: _range = xrange + _str = unicode if sys.version_info[1] >= 7: _viewkeys = dict.viewkeys else: @@ -118,7 +120,7 @@ def __init__(self, path, key, value): self.value = value def __repr__(self): - return str(self.get()) + return _str(self.get()) class _op_add(_op_base): def _on_undo_remove(self, path, key): @@ -207,11 +209,11 @@ def get(self): return {'op': 'move', 'path': _path_join(self.path, self.key), 'from': _path_join(self.oldpath, self.oldkey)} def __repr__(self): - return str(self.get()) + return _str(self.get()) def _path_join(path, key): if key != None: - return path + '/' + str(key).replace('~', '~0').replace('/', '~1') + return path + '/' + _str(key).replace('~', '~0').replace('/', '~1') return path def _item_added(path, key, info, item): @@ -257,9 +259,9 @@ def _compare_dicts(path, info, src, dst): added_keys = dst_keys - src_keys removed_keys = src_keys - dst_keys for key in removed_keys: - _item_removed(path, str(key), info, src[key]) + _item_removed(path, _str(key), info, src[key]) for key in added_keys: - _item_added(path, str(key), info, dst[key]) + _item_added(path, _str(key), info, dst[key]) for key in src_keys & dst_keys: _compare_values(path, key, info, src[key], dst[key]) From 282bebae977021b6f1d19fad0c967eb625af7331 Mon Sep 17 00:00:00 2001 From: selurvedu Date: Fri, 14 Aug 2015 15:45:03 +0000 Subject: [PATCH 27/39] Extend tests that check list patching --- tests.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests.py b/tests.py index 5b0d9e9..bb9e296 100755 --- a/tests.py +++ b/tests.py @@ -327,13 +327,17 @@ def test_use_move_instead_of_remove_add(self): self.assertEqual(res, dst) def test_use_move_instead_of_add_remove(self): - src = {'foo': [1, 2, 3]} - dst = {'foo': [3, 1, 2]} - patch = list(jsonpatch.make_patch(src, dst)) - self.assertEqual(len(patch), 1) - self.assertEqual(patch[0]['op'], 'move') - res = jsonpatch.apply_patch(src, patch) - self.assertEqual(res, dst) + def fn(_src, _dst): + patch = list(jsonpatch.make_patch(_src, _dst)) + self.assertEqual(len(patch), 1) + self.assertEqual(patch[0]['op'], 'move') + res = jsonpatch.apply_patch(_src, patch) + self.assertEqual(res, _dst) + + fn({'foo': [1, 2, 3]}, {'foo': [3, 1, 2]}) + fn({'foo': [1, 2, 3]}, {'foo': [3, 2, 1]}) + fn([1, 2, 3], [3, 1, 2]) + fn([1, 2, 3], [3, 2, 1]) def test_escape(self): src = {"x/y": 1} From c1067351a798534dda59aadccdfe5eb96fd520b4 Mon Sep 17 00:00:00 2001 From: selurvedu Date: Wed, 19 Aug 2015 17:00:16 +0000 Subject: [PATCH 28/39] Allow longer patches in test_use_move_... --- tests.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests.py b/tests.py index bb9e296..c5134d3 100755 --- a/tests.py +++ b/tests.py @@ -329,8 +329,9 @@ def test_use_move_instead_of_remove_add(self): def test_use_move_instead_of_add_remove(self): def fn(_src, _dst): patch = list(jsonpatch.make_patch(_src, _dst)) - self.assertEqual(len(patch), 1) - self.assertEqual(patch[0]['op'], 'move') + # Check if there are only 'move' operations + for p in patch: + self.assertEqual(p['op'], 'move') res = jsonpatch.apply_patch(_src, patch) self.assertEqual(res, _dst) From 6761340d8b4eae3419eebfb202c09f9956387148 Mon Sep 17 00:00:00 2001 From: Stas Erema Date: Tue, 18 Aug 2015 16:28:07 +0300 Subject: [PATCH 29/39] added error-prone cases to default tests --- tests.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests.py b/tests.py index c5134d3..8da9cfc 100755 --- a/tests.py +++ b/tests.py @@ -356,6 +356,38 @@ def test_root_list(self): res = patch.apply(src) self.assertEqual(res, dst) + def test_fail_prone_list_1(self): + """ Test making and applying a patch of the root is a list """ + src = [u'a', u'r', u'b'] + dst = [u'b', u'o'] + patch = jsonpatch.make_patch(src, dst) + res = patch.apply(src) + self.assertEqual(res, dst) + + def test_fail_prone_list_2(self): + """ Test making and applying a patch of the root is a list """ + src = [u'a', u'r', u'b', u'x', u'm', u'n'] + dst = [u'b', u'o', u'm', u'n'] + patch = jsonpatch.make_patch(src, dst) + res = patch.apply(src) + self.assertEqual(res, dst) + + def test_fail_prone_list_3(self): + """ Test making and applying a patch of the root is a list """ + src = [u'boo1', u'bar', u'foo1', u'qux'] + dst = [u'qux', u'bar'] + patch = jsonpatch.make_patch(src, dst) + res = patch.apply(src) + self.assertEqual(res, dst) + + def test_fail_prone_list_4(self): + """ Test making and applying a patch of the root is a list """ + src = [u'bar1', 59, u'foo1', u'foo'] + dst = [u'foo', u'bar', u'foo1'] + patch = jsonpatch.make_patch(src, dst) + res = patch.apply(src) + self.assertEqual(res, dst) + class InvalidInputTests(unittest.TestCase): From 0734d45eb651ed9dd1d37b6b80265fc3a6263607 Mon Sep 17 00:00:00 2001 From: selurvedu Date: Sun, 18 Oct 2015 21:17:38 +0000 Subject: [PATCH 30/39] Update tests from ea80865 to run on Python 3.2 --- tests.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests.py b/tests.py index 8da9cfc..d2f9fe9 100755 --- a/tests.py +++ b/tests.py @@ -358,32 +358,32 @@ def test_root_list(self): def test_fail_prone_list_1(self): """ Test making and applying a patch of the root is a list """ - src = [u'a', u'r', u'b'] - dst = [u'b', u'o'] + src = ['a', 'r', 'b'] + dst = ['b', 'o'] patch = jsonpatch.make_patch(src, dst) res = patch.apply(src) self.assertEqual(res, dst) def test_fail_prone_list_2(self): """ Test making and applying a patch of the root is a list """ - src = [u'a', u'r', u'b', u'x', u'm', u'n'] - dst = [u'b', u'o', u'm', u'n'] + src = ['a', 'r', 'b', 'x', 'm', 'n'] + dst = ['b', 'o', 'm', 'n'] patch = jsonpatch.make_patch(src, dst) res = patch.apply(src) self.assertEqual(res, dst) def test_fail_prone_list_3(self): """ Test making and applying a patch of the root is a list """ - src = [u'boo1', u'bar', u'foo1', u'qux'] - dst = [u'qux', u'bar'] + src = ['boo1', 'bar', 'foo1', 'qux'] + dst = ['qux', 'bar'] patch = jsonpatch.make_patch(src, dst) res = patch.apply(src) self.assertEqual(res, dst) def test_fail_prone_list_4(self): """ Test making and applying a patch of the root is a list """ - src = [u'bar1', 59, u'foo1', u'foo'] - dst = [u'foo', u'bar', u'foo1'] + src = ['bar1', 59, 'foo1', 'foo'] + dst = ['foo', 'bar', 'foo1'] patch = jsonpatch.make_patch(src, dst) res = patch.apply(src) self.assertEqual(res, dst) From 73acf7ff2a83c411217aff024d35cc8b602180ed Mon Sep 17 00:00:00 2001 From: selurvedu Date: Fri, 8 Apr 2016 04:24:29 +0000 Subject: [PATCH 31/39] Move list-related testcases into separate class --- tests.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests.py b/tests.py index d2f9fe9..b03b64b 100755 --- a/tests.py +++ b/tests.py @@ -356,6 +356,9 @@ def test_root_list(self): res = patch.apply(src) self.assertEqual(res, dst) + +class ListTests(unittest.TestCase): + def test_fail_prone_list_1(self): """ Test making and applying a patch of the root is a list """ src = ['a', 'r', 'b'] @@ -453,6 +456,7 @@ def get_suite(): suite.addTest(unittest.makeSuite(ApplyPatchTestCase)) suite.addTest(unittest.makeSuite(EqualityTestCase)) suite.addTest(unittest.makeSuite(MakePatchTestCase)) + suite.addTest(unittest.makeSuite(ListTests)) suite.addTest(unittest.makeSuite(InvalidInputTests)) suite.addTest(unittest.makeSuite(ConflictTests)) return suite From 23a3400f480784db580272e8484a744384c867ea Mon Sep 17 00:00:00 2001 From: Roman Kozlovskyi Date: Mon, 16 Feb 2015 13:53:17 +0200 Subject: [PATCH 32/39] Added optimization replace operation --- jsonpatch.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index e993020..936ee74 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -696,7 +696,8 @@ def _compare_left(path, src, left, shift): # yes, there should be any value field, but we'll use it # to apply `move` optimization a bit later and will remove # it in _optimize function. - 'value': src[idx - shift], + #'value': src[idx - shift], + 'value': src[idx - shift] if shift>0 else src[idx], 'path': ptr.path, }, shift - 1 @@ -732,12 +733,15 @@ def _optimize(operations): result = [] ops_by_path = {} ops_by_value = {} + ops_to_replace = {} add_remove = set(['add', 'remove']) for item in operations: # could we apply "move" optimization for dict values? hashable_value = not isinstance(item['value'], (MutableMapping, MutableSequence)) if item['path'] in ops_by_path: + if item['op'] == 'add': + ops_to_replace[item['path']] = (ops_to_replace[item['path']][0], item['value']) _optimize_using_replace(ops_by_path[item['path']], item) continue if hashable_value and item['value'] in ops_by_value: @@ -751,6 +755,17 @@ def _optimize(operations): ops_by_path[item['path']] = item if hashable_value: ops_by_value[item['value']] = item + if item['op'] == 'remove': + ops_to_replace[item['path']] = (item['value'], None) + + for i in result: + if i['path'] in ops_to_replace and ops_to_replace[i['path']][1]: + patch = make_patch(*ops_to_replace[i['path']]).patch + for x in patch: + x['path'] = '{}{}'.format(i['path'], x['path']) + x['value'] = x.get('value', '') + index = result.index(i) + result[index:index+1] = patch # cleanup ops_by_path.clear() From 3d750cfd35537fa0289856bb2fb77138229ad4bb Mon Sep 17 00:00:00 2001 From: Roman Kozlovskyi Date: Mon, 16 Feb 2015 14:01:54 +0200 Subject: [PATCH 33/39] Fixed for python 2.6 --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index 936ee74..bcf66ea 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -762,7 +762,7 @@ def _optimize(operations): if i['path'] in ops_to_replace and ops_to_replace[i['path']][1]: patch = make_patch(*ops_to_replace[i['path']]).patch for x in patch: - x['path'] = '{}{}'.format(i['path'], x['path']) + x['path'] = i['path'] + x['path'] x['value'] = x.get('value', '') index = result.index(i) result[index:index+1] = patch From 42f610403129e4e332a871438656bc5b6cf87dd9 Mon Sep 17 00:00:00 2001 From: Roman Kozlovskyi Date: Thu, 19 Jan 2017 12:43:33 +0200 Subject: [PATCH 34/39] Fixed replace optimization in jsondiff --- jsondiff.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jsondiff.py b/jsondiff.py index bfa60af..69696c1 100644 --- a/jsondiff.py +++ b/jsondiff.py @@ -107,7 +107,10 @@ def execute(self): op_first.path == op_second.path and \ type(op_first) == _op_remove and \ type(op_second) == _op_add: - yield _op_replace(op_second.path, op_second.key, op_second.value).get() + info = _compare_info() + _compare_values(op_second.path, op_second.key, info, op_first.value, op_second.value) + for i in info.execute(): + yield i curr = curr[1][1] continue yield curr[2].get() From fd66fb5a567279d44f9dbe55c16f56d892933ff5 Mon Sep 17 00:00:00 2001 From: Roman Kozlovskyi Date: Thu, 19 Jan 2017 14:32:13 +0200 Subject: [PATCH 35/39] Split add-remove optimization test --- tests.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests.py b/tests.py index 8b0b52c..a81729c 100755 --- a/tests.py +++ b/tests.py @@ -301,7 +301,16 @@ def test_add_nested(self): def test_should_just_add_new_item_not_rebuild_all_list(self): src = {'foo': [1, 2, 3]} - dst = {'foo': [3, 1, 2, 3]} + dst = {'foo': [4, 1, 2, 3]} + patch = list(jsonpatch.make_patch(src, dst)) + self.assertEqual(len(patch), 1) + self.assertEqual(patch[0]['op'], 'add') + res = jsonpatch.apply_patch(src, patch) + self.assertEqual(res, dst) + + def test_should_add_instead_of_move_add(self): + src = [1, 2] + dst = [2, 1, 2] patch = list(jsonpatch.make_patch(src, dst)) self.assertEqual(len(patch), 1) self.assertEqual(patch[0]['op'], 'add') From ae116aeb71d1686b64fd87edf94115a2c658b9ab Mon Sep 17 00:00:00 2001 From: Roman Kozlovskyi Date: Tue, 24 Jan 2017 15:03:22 +0200 Subject: [PATCH 36/39] Fixed move in-place --- jsondiff.py | 3 ++- tests.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/jsondiff.py b/jsondiff.py index 69696c1..cb19639 100644 --- a/jsondiff.py +++ b/jsondiff.py @@ -113,7 +113,8 @@ def execute(self): yield i curr = curr[1][1] continue - yield curr[2].get() + if type(curr[2]) != _op_move or curr[2].oldpath != curr[2].path or curr[2].oldkey != curr[2].key: + yield curr[2].get() curr = curr[1] class _op_base(object): diff --git a/tests.py b/tests.py index 7152689..303e378 100755 --- a/tests.py +++ b/tests.py @@ -317,6 +317,25 @@ def test_should_add_instead_of_move_add(self): res = jsonpatch.apply_patch(src, patch) self.assertEqual(res, dst) + def test_should_add_instead_of_move_add2(self): + src = [2, 4, 5] + dst = [1, 2, 3, 4, 5] + patch = list(jsonpatch.make_patch(src, dst)) + self.assertEqual(len(patch), 2) + self.assertEqual(patch[0]['op'], 'add') + res = jsonpatch.apply_patch(src, patch) + self.assertEqual(res, dst) + + def test_should_remove_instead_of_remove_move(self): + src = [1, 2, 3, 4, 5] + dst = [2, 4, 5] + patch = list(jsonpatch.make_patch(src, dst)) + self.assertEqual(len(patch), 2) + self.assertEqual(patch[0]['op'], 'remove') + self.assertEqual(patch[1]['op'], 'remove') + res = jsonpatch.apply_patch(src, patch) + self.assertEqual(res, dst) + def test_use_replace_instead_of_remove_add(self): src = {'foo': [1, 2, 3]} dst = {'foo': [3, 2, 3]} From 3bc8a6be46071f8f51fa25571e25e64d46000cdb Mon Sep 17 00:00:00 2001 From: Roman Kozlovskyi Date: Wed, 25 Jan 2017 14:30:11 +0200 Subject: [PATCH 37/39] Added test from origin --- tests.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests.py b/tests.py index 303e378..f3f6460 100755 --- a/tests.py +++ b/tests.py @@ -299,6 +299,15 @@ def test_add_nested(self): } self.assertEqual(expected, res) + def test_use_replace_instead_of_remove_add_nested(self): + src = {'foo': [{'bar': 1, 'baz': 2}, {'bar': 2, 'baz': 3}]} + dst = {'foo': [{'bar': 1}, {'bar': 2, 'baz': 3}]} + patch = list(jsonpatch.make_patch(src, dst)) + self.assertEqual(len(patch), 1) + self.assertEqual(patch[0]['op'], 'remove') + res = jsonpatch.apply_patch(src, patch) + self.assertEqual(res, dst) + def test_should_just_add_new_item_not_rebuild_all_list(self): src = {'foo': [1, 2, 3]} dst = {'foo': [4, 1, 2, 3]} From 3b0393c321d533c1952cf5e050ba80b14f0c1b17 Mon Sep 17 00:00:00 2001 From: Roman Kozlovskyi Date: Wed, 25 Jan 2017 14:35:30 +0200 Subject: [PATCH 38/39] Update version --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index bcf66ea..f7deaef 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -53,7 +53,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.13+jsondiff.0' +__version__ = '1.13+jsondiff.unicode.replacefix.0' __website__ = 'https://github.com/stefankoegl/python-json-patch' __license__ = 'Modified BSD License' From d7a53248617b4b840431ef49487db86cfb901487 Mon Sep 17 00:00:00 2001 From: Volodymyr Date: Wed, 2 Dec 2020 16:34:19 +0200 Subject: [PATCH 39/39] fixing python3 support --- jsonpatch.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/jsonpatch.py b/jsonpatch.py index f7deaef..80b4aa6 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -37,7 +37,6 @@ import collections import copy import functools -import inspect import itertools import json import sys @@ -53,7 +52,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.13+jsondiff.unicode.replacefix.0' +__version__ = '1.14+jsondiff.unicode.replacefix.0' __website__ = 'https://github.com/stefankoegl/python-json-patch' __license__ = 'Modified BSD License' @@ -99,20 +98,11 @@ def multidict(ordered_pairs): def get_loadjson(): - """ adds the object_pairs_hook parameter to json.load when possible - - The "object_pairs_hook" parameter is used to handle duplicate keys when - loading a JSON object. This parameter does not exist in Python 2.6. This - methods returns an unmodified json.load for Python 2.6 and a partial - function with object_pairs_hook set to multidict for Python versions that - support the parameter. """ - - argspec = inspect.getargspec(json.load) - if 'object_pairs_hook' not in argspec.args: - return json.load + """ adds the object_pairs_hook parameter to json.load """ return functools.partial(json.load, object_pairs_hook=multidict) + json.load = get_loadjson()