Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to generate test operations in diff #154

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion bin/jsondiff
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ parser.add_argument('-u', '--preserve-unicode', action='store_true',
help='Output Unicode character as-is without using Code Point')
parser.add_argument('-v', '--version', action='version',
version='%(prog)s ' + jsonpatch.__version__)
parser.add_argument('-t', '--test-ops', action='store_true',
help='Generate before-state test ops for remove/replace')


def main():
Expand All @@ -32,7 +34,7 @@ def diff_files():
args = parser.parse_args()
doc1 = json.load(args.FILE1)
doc2 = json.load(args.FILE2)
patch = jsonpatch.make_patch(doc1, doc2)
patch = jsonpatch.make_patch(doc1, doc2, generate_test_ops=args.test_ops)
if patch.patch:
print(json.dumps(patch.patch, indent=args.indent, ensure_ascii=not(args.preserve_unicode)))
sys.exit(1)
Expand Down
55 changes: 48 additions & 7 deletions jsonpatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def apply_patch(doc, patch, in_place=False, pointer_cls=JsonPointer):
return patch.apply(doc, in_place)


def make_patch(src, dst, pointer_cls=JsonPointer):
def make_patch(src, dst, generate_test_ops=False, pointer_cls=JsonPointer):
"""Generates patch by comparing two document objects. Actually is
a proxy to :meth:`JsonPatch.from_diff` method.

Expand All @@ -170,6 +170,12 @@ def make_patch(src, dst, pointer_cls=JsonPointer):
:param pointer_cls: JSON pointer class to use.
:type pointer_cls: Type[JsonPointer]

:param generate_test_ops: While :const:`True` generate test operations
capturing previous values of `replace`/`remove`
operations. By default do not generate these
operations.
:type generate_test_ops: bool

>>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]}
>>> dst = {'baz': 'qux', 'numbers': [1, 4, 7]}
>>> patch = make_patch(src, dst)
Expand All @@ -178,7 +184,7 @@ def make_patch(src, dst, pointer_cls=JsonPointer):
True
"""

return JsonPatch.from_diff(src, dst, pointer_cls=pointer_cls)
return JsonPatch.from_diff(src, dst, generate_test_ops=generate_test_ops, pointer_cls=pointer_cls)


class PatchOperation(object):
Expand Down Expand Up @@ -475,6 +481,11 @@ def apply(self, obj):

return obj

def _on_undo_add(self, path, key):
return key

def _on_undo_remove(self, path, key):
return key

class CopyOperation(PatchOperation):
""" Copies an object property or an array element to a new location """
Expand Down Expand Up @@ -628,7 +639,7 @@ def from_string(cls, patch_str, loads=None, pointer_cls=JsonPointer):

@classmethod
def from_diff(
cls, src, dst, optimization=True, dumps=None,
cls, src, dst, generate_test_ops=False, dumps=None,
pointer_cls=JsonPointer,
):
"""Creates JsonPatch instance based on comparison of two document
Expand All @@ -641,6 +652,12 @@ def from_diff(
:param dst: Data source document object.
:type dst: dict

:param generate_test_ops: While :const:`True` generate test operations
capturing previous values of `replace`/`remove`
operations. By default do not generate these
operations.
:type generate_test_ops: bool

:param dumps: A function of one argument that produces a serialized
JSON string.
:type dumps: function
Expand All @@ -658,7 +675,7 @@ def from_diff(
True
"""
json_dumper = dumps or cls.json_dumper
builder = DiffBuilder(src, dst, json_dumper, pointer_cls=pointer_cls)
builder = DiffBuilder(src, dst, json_dumper, generate_test_ops=generate_test_ops, pointer_cls=pointer_cls)
builder._compare_values('', None, src, dst)
ops = list(builder.execute())
return cls(ops, pointer_cls=pointer_cls)
Expand Down Expand Up @@ -711,9 +728,10 @@ def _get_operation(self, operation):

class DiffBuilder(object):

def __init__(self, src_doc, dst_doc, dumps=json.dumps, pointer_cls=JsonPointer):
def __init__(self, src_doc, dst_doc, dumps=json.dumps, generate_test_ops=False, pointer_cls=JsonPointer):
self.dumps = dumps
self.pointer_cls = pointer_cls
self.generate_test_ops = generate_test_ops
self.index_storage = [{}, {}]
self.index_storage2 = [[], []]
self.__root = root = []
Expand Down Expand Up @@ -801,6 +819,11 @@ def _item_added(self, path, key, item):
for v in self.iter_from(index):
op.key = v._on_undo_remove(op.path, op.key)

if self.generate_test_ops:
prev_op_index = index[0]
if isinstance(prev_op_index[2], TestOperation):
self.remove(prev_op_index)

self.remove(index)
if op.location != _path_join(path, key):
new_op = MoveOperation({
Expand All @@ -819,6 +842,13 @@ def _item_added(self, path, key, item):
self.store_index(item, new_index, _ST_ADD)

def _item_removed(self, path, key, item):
if self.generate_test_ops:
test_index = self.insert(TestOperation({
'op': 'test',
'path': _path_join(path, key),
'value': item,
}, pointer_cls=self.pointer_cls))

new_op = RemoveOperation({
'op': 'remove',
'path': _path_join(path, key),
Expand All @@ -844,14 +874,25 @@ def _item_removed(self, path, key, item):
'path': op.location,
}, pointer_cls=self.pointer_cls)
new_index[2] = new_op
if self.generate_test_ops:
self.remove(test_index)

else:
self.remove(new_index)
if self.generate_test_ops:
self.remove(test_index)

else:
self.store_index(item, new_index, _ST_REMOVE)

def _item_replaced(self, path, key, item):
def _item_replaced(self, path, key, item, old_item):
if self.generate_test_ops:
self.insert(TestOperation({
'op': 'test',
'path': _path_join(path, key),
'value': old_item,
}, pointer_cls=self.pointer_cls))

self.insert(ReplaceOperation({
'op': 'replace',
'path': _path_join(path, key),
Expand Down Expand Up @@ -921,7 +962,7 @@ def _compare_values(self, path, key, src, dst):
return

else:
self._item_replaced(path, key, dst)
self._item_replaced(path, key, dst, src)


def _path_join(path, key):
Expand Down
Loading