Skip to content
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
28 changes: 24 additions & 4 deletions jsonpatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,23 @@ def _reorder(mapping, keys):
mapping[key] = mapping.pop(key)


def _plain_operation(operation, pointer_cls):
"""Returns operation as a dict whose 'path' and 'from' are strings.

Both can be given as pointer objects instead. These are replaced by
their paths, so that the operation is JSON-serializable, and can be
compared to one that was given the paths. This is done in a copy; an
operation that is a dict already and has no pointer objects is returned
as it is.
"""
paths = {member: operation[member].path for member in ('path', 'from')
if member in operation and
isinstance(operation[member], pointer_cls)}
if paths:
return dict(operation, **paths)
return operation if isinstance(operation, dict) else dict(operation)


class PatchOperation:
"""A single operation inside a JSON Patch."""

Expand Down Expand Up @@ -491,13 +508,17 @@ def _apply(self, obj, journal):
def __hash__(self):
# Other members can hold arrays or objects, which are unhashable.
# Operations that compare equal still have equal 'op' and 'path'.
return hash((self.operation.get('op'), self.operation['path']))
operation = _plain_operation(self.operation, self.pointer_cls)
return hash((operation.get('op'), operation['path']))

def __eq__(self, other):
if not isinstance(other, PatchOperation):
return False
# e.g. tests for 1 and for true behave differently
return _json_equal(self.operation, other.operation)
# and paths are equal to pointer objects for them
return _json_equal(
_plain_operation(self.operation, self.pointer_cls),
_plain_operation(other.operation, other.pointer_cls))

@property
def path(self):
Expand Down Expand Up @@ -962,8 +983,7 @@ def __init__(self, patch, pointer_cls=JsonPointer):
self._get_operation(op)
# Keep validation errors tied to the original input, but expose
# concrete, JSON-serializable operation dictionaries on success.
if not isinstance(op, dict):
self.patch[index] = dict(op)
self.patch[index] = _plain_operation(op, self.pointer_cls)

def __str__(self):
"""str(self) -> self.to_string()"""
Expand Down
89 changes: 89 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1967,6 +1967,95 @@ def test_copy_from_pointer(self):
])
self.assertEqual(patch.apply({'foo': 1}), {'foo': 1, 'bar': 1})

def pointer_and_string_operations(self, pointer_cls=jsonpointer.JsonPointer,
paths=('/foo', '/bar/0', '')):
foo, bar, root = paths
with_strings = [
{'op': 'add', 'path': foo, 'value': [1]},
{'op': 'move', 'from': foo, 'path': bar},
{'op': 'copy', 'from': root, 'path': foo},
{'op': 'copy', 'from': foo, 'path': bar},
]
with_pointers = [
{'op': 'add', 'path': pointer_cls(foo), 'value': [1]},
{'op': 'move', 'from': pointer_cls(foo), 'path': pointer_cls(bar)},
{'op': 'copy', 'from': pointer_cls(root), 'path': foo},
{'op': 'copy', 'from': foo, 'path': pointer_cls(bar)},
]
return with_pointers, with_strings

def test_patch_with_pointers_is_serializable(self):
with_pointers, with_strings = self.pointer_and_string_operations()
patch = jsonpatch.JsonPatch(with_pointers)
self.assertEqual(patch.patch, with_strings)
self.assertEqual(list(patch), with_strings)
self.assertEqual(json.loads(patch.to_string()), with_strings)
self.assertEqual(json.loads(str(patch)), with_strings)
self.assertEqual(jsonpatch.JsonPatch.from_string(patch.to_string()), patch)

def test_patch_with_pointers_equals_patch_with_strings(self):
with_pointers, with_strings = self.pointer_and_string_operations()
patch = jsonpatch.JsonPatch(with_pointers)
other = jsonpatch.JsonPatch(with_strings)
self.assertEqual(patch, other)
self.assertEqual(hash(patch), hash(other))
self.assertEqual(len({patch, other}), 1)
doc = {'bar': []}
self.assertEqual(patch.apply(doc), other.apply(doc))

def test_patch_with_pointers_does_not_change_the_operations(self):
with_pointers, _ = self.pointer_and_string_operations()
pointers = [(op, member, op[member]) for op in with_pointers
for member in ('path', 'from')
if isinstance(op.get(member), jsonpointer.JsonPointer)]
jsonpatch.JsonPatch(with_pointers).apply({'bar': []})
self.assertEqual(len(pointers), 5)
for op, member, pointer in pointers:
self.assertIs(op[member], pointer)

def test_patch_with_pointers_as_read_only_mappings(self):
with_pointers, with_strings = self.pointer_and_string_operations()
patch = jsonpatch.JsonPatch(map(MappingProxyType, with_pointers))
self.assertEqual(patch.patch, with_strings)
self.assertTrue(all(type(op) is dict for op in patch))

def test_patch_with_pointers_of_custom_class(self):
with_pointers, with_strings = self.pointer_and_string_operations(
DotJsonPointer, ('foo', 'bar.0', ''))
patch = jsonpatch.JsonPatch(with_pointers, pointer_cls=DotJsonPointer)
other = jsonpatch.JsonPatch(with_strings, pointer_cls=DotJsonPointer)
self.assertEqual(patch.patch, with_strings)
self.assertEqual(patch, other)
self.assertEqual(patch.apply({'bar': []}), other.apply({'bar': []}))

def test_operation_with_pointers_equals_operation_with_strings(self):
with_pointers, with_strings = self.pointer_and_string_operations()
for pointers, strings in zip(with_pointers, with_strings):
with self.subTest(operation=strings):
cls = jsonpatch.JsonPatch.operations[strings['op']]
operation, other = cls(pointers), cls(strings)
self.assertEqual(operation, other)
self.assertEqual(other, operation)
self.assertEqual(hash(operation), hash(other))
self.assertEqual(operation.location, other.location)
# setters change the operation it was created from, so it
# is not replaced by a copy without pointers
self.assertIs(operation.operation, pointers)
self.assertNotEqual(
jsonpatch.RemoveOperation({'op': 'remove', 'path': jsonpointer.JsonPointer('/a')}),
jsonpatch.RemoveOperation({'op': 'remove', 'path': '/b'}))

def test_operation_setters_with_pointers(self):
pointers = {'op': 'move', 'from': jsonpointer.JsonPointer('/a/b'),
'path': jsonpointer.JsonPointer('/c/d')}
operation = jsonpatch.MoveOperation(pointers)
operation.key = 'e'
operation.from_key = 'f'
self.assertEqual(pointers,
{'op': 'move', 'from': '/a/f', 'path': '/c/e'})
self.assertEqual(operation.apply({'a': {'f': 1}, 'c': {}}),
{'a': {}, 'c': {'e': 1}})


class JsonPatchCreationTest(unittest.TestCase):

Expand Down
Loading