Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
50a5e0d
Enable container-based builds on Travis-CI
stefankoegl Apr 23, 2015
5d1980d
Fix make_patch with unicode paths
stefankoegl May 7, 2015
b8275b5
Require pypandoc instead of pandoc for setup.py
stefankoegl May 7, 2015
348ec1d
bump version to 1.10
stefankoegl May 7, 2015
23f9b1a
Fix tests for Python 3.2
stefankoegl May 7, 2015
dc9260e
bump version to 1.11
stefankoegl May 8, 2015
5ee8e9e
Add Travis tests for Python 3.5
stefankoegl Sep 18, 2015
70306fa
Fix build failure on Travis CI with Python 3.2
selurvedu Oct 18, 2015
a2ba02f
Merge pull request #45 from selurvedu/py32_coverage
stefankoegl Oct 22, 2015
0ae80ba
Allow running certain tests separately
selurvedu Aug 14, 2015
99bf616
Merge pull request #43 from selurvedu/separate-tests
stefankoegl Oct 28, 2015
3f2328e
Fix bug in _split_by_common_seq using wrong range in right subtree
Nov 3, 2015
2a02d21
Add failing test for #40
stefankoegl Nov 3, 2015
46ad04c
Merge pull request #46 from apinkney97/master, fixes #40
stefankoegl Nov 3, 2015
d877f1d
bump version to 1.12
stefankoegl Nov 3, 2015
5c2a9b9
Add encoding info to setup.py
stefankoegl Feb 13, 2016
32dcbb0
Create "universal" wheel packages
stefankoegl Feb 13, 2016
a33021b
Optimize "deep" ``replace`` operation, fixes #36
stefankoegl Feb 13, 2016
4443d32
Make ``move`` operation with from == path a no-op
stefankoegl Feb 13, 2016
cf0da04
Print test comments when external tests fail
stefankoegl Feb 13, 2016
9ce8487
Remove import of ``pudb``
stefankoegl Feb 13, 2016
b15d8f1
bump version to 1.13
stefankoegl Feb 13, 2016
65dc01c
Import jsondiff.py from nxofsys/jsondiff@96e82b
selurvedu Oct 14, 2015
eeb6df0
Generate patches with jsondiff.py
selurvedu Oct 14, 2015
e3e5e0b
Add jsondiff into setup.py
selurvedu Apr 8, 2016
0a0f1b2
Tag: v1.9+jsondiff.0
selurvedu Apr 8, 2016
315823c
Update to upstream rev.: nxsofsys/jsondiff@7cb4ef9
selurvedu Oct 15, 2015
3296da3
Tag: v1.9+jsondiff.1
selurvedu Apr 8, 2016
6b73e3d
Tag: v1.11+jsondiff.0
selurvedu Apr 8, 2016
2cb56d0
Tag: v1.12+jsondiff.0
selurvedu Apr 8, 2016
acfdb94
Tag: v1.13+jsondiff.0
selurvedu Apr 8, 2016
a059d06
Use unicode() instead of str() on Python 2
selurvedu Apr 8, 2016
282beba
Extend tests that check list patching
selurvedu Aug 14, 2015
c106735
Allow longer patches in test_use_move_...
selurvedu Aug 19, 2015
6761340
added error-prone cases to default tests
tyerq Aug 18, 2015
0734d45
Update tests from ea80865 to run on Python 3.2
selurvedu Oct 18, 2015
73acf7f
Move list-related testcases into separate class
selurvedu Apr 8, 2016
a2cd6da
Merge branch 'list_tests@v1.9' into list_tests@v1.13
selurvedu Apr 8, 2016
23a3400
Added optimization replace operation
kroman0 Feb 16, 2015
3d750cf
Fixed for python 2.6
kroman0 Feb 16, 2015
42f6104
Fixed replace optimization in jsondiff
kroman0 Jan 19, 2017
fd66fb5
Split add-remove optimization test
kroman0 Jan 19, 2017
fc5220a
Merge remote-tracking branch 'selurvedu/list_tests@v1.13' into v1.13+…
kroman0 Jan 19, 2017
ae116ae
Fixed move in-place
kroman0 Jan 24, 2017
3bc8a6b
Added test from origin
kroman0 Jan 25, 2017
3b0393c
Update version
kroman0 Jan 25, 2017
d7a5324
fixing python3 support
Dec 2, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ python:
- "2.7"
- "3.2"
- "3.3"
- "3.4"
- "3.4"
- "3.5"
- "pypy"
- "pypy3"

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:
- coverage run --source=jsonpatch tests.py

after_script:
- coveralls

sudo: false
7 changes: 5 additions & 2 deletions ext_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
303 changes: 303 additions & 0 deletions jsondiff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
'''
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.
'''

import sys

__all__ = ["make",]

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:
_viewkeys = lambda x: set(dict.keys(x))

_ST_ADD = 0
_ST_REMOVE = 1

class _compare_info(object):

def __init__(self):
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]
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:
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
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):
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 = info.take_index(item, _ST_REMOVE)
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)
info.store_index(item, new_index, _ST_ADD)

def _item_removed(path, key, info, item):
new_op = _op_remove(path, key, item)
index = info.take_index(item, _ST_ADD)
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:
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):
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_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 _range(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()]
Loading