diff --git a/jsonpatch.py b/jsonpatch.py index 9ff14d5..03a7b31 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -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.""" @@ -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): @@ -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()""" diff --git a/tests.py b/tests.py index f0d7581..b9ec438 100644 --- a/tests.py +++ b/tests.py @@ -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):