Skip to content
Merged
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
24 changes: 22 additions & 2 deletions jsonpatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ def __hash__(self):
def __eq__(self, other):
if not isinstance(other, PatchOperation):
return False
return self.operation == other.operation
# e.g. tests for 1 and for true behave differently
return _json_equal(self.operation, other.operation)

def __ne__(self, other):
return not(self == other)
Expand Down Expand Up @@ -410,7 +411,7 @@ def apply(self, obj):
raise InvalidJsonPatch(
"The operation does not contain a 'value' member")

if val != value:
if not _json_equal(val, value):
msg = '{0} ({1}) is not equal to tested value {2} ({3})'
raise JsonPatchTestFailed(msg.format(val, type(val),
value, type(value)))
Expand Down Expand Up @@ -961,6 +962,25 @@ def _positional_cost(old, new):
return 1


def _json_equal(first, second):
""" Compares values the way RFC 6902, 4.6 does: numbers by value, but
literals like true only with themselves, which Python considers equal to
1, and arrays and objects member by member """
if isinstance(first, bool) or isinstance(second, bool):
return isinstance(first, bool) and isinstance(second, bool) and \
first == second
if isinstance(first, MutableMapping) and \
isinstance(second, MutableMapping):
return len(first) == len(second) and all(
key in second and _json_equal(first[key], second[key])
for key in first)
if isinstance(first, MutableSequence) and \
isinstance(second, MutableSequence):
return len(first) == len(second) and all(
_json_equal(*items) for items in zip(first, second))
return first == second


def _sorted_members(value):
""" Copies the arrays and objects in value, with the members of objects
sorted by key """
Expand Down
8 changes: 5 additions & 3 deletions property_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,11 +427,13 @@ def test_replace_any_existing_location(self, case, value):
doc, [{'op': 'replace', 'path': to_pointer(parts), 'value': value}])
assert_json_equal(resolve(result, parts), value)

# RFC 6902, 4.6: literals like true are only equal to themselves, but
# 'test' uses Python equality, which considers 1 and True equal
@unittest.expectedFailure
# RFC 6902, 4.6: literals like true are only equal to themselves, although
# Python considers 1 and True equal, #216
@given(doc_pairs)
@example(values=(1, True))
@example(values=([1], [True]))
@example(values=({'a': 0}, {'a': False}))
@example(values=(1, 1.0))
def test_test_operation_uses_json_equality(self, values):
value, tested = values
result = outcome(jsonpatch.apply_patch, {'a': value},
Expand Down
28 changes: 28 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,23 @@ def test_test_error(self):
jsonpatch.apply_patch,
obj, [{'op': 'test', 'path': '/bar', 'value': 'bar'}])

def test_test_numbers_by_value(self):
obj = {'a': 1, 'b': [0.0], 'c': {'d': 2}}
jsonpatch.apply_patch(obj, [{'op': 'test', 'path': '/a', 'value': 1.0},
{'op': 'test', 'path': '/b', 'value': [0]},
{'op': 'test', 'path': '/c', 'value': {'d': 2.0}}])

def test_test_literals_differ_from_numbers(self):
# RFC 6902, 4.6: true and false are only equal to themselves, #216
for doc, value in [({'a': 1}, True),
({'a': [1]}, [True]),
({'a': {'a': 0}}, {'a': False}),
({'a': True}, 1),
({'a': False}, 0)]:
with self.assertRaises(jsonpatch.JsonPatchTestFailed):
jsonpatch.apply_patch(
doc, [{'op': 'test', 'path': '/a', 'value': value}])


def test_test_not_existing(self):
obj = {'bar': 'qux'}
Expand Down Expand Up @@ -363,6 +380,17 @@ def test_patch_unequal(self):
patch2 = jsonpatch.JsonPatch([{'op': 'test', 'path': '/test1'}])
self.assertNotEqual(patch1, patch2)

def test_patch_unequal_literal_and_number(self):
# the patches behave differently, #216
patch1 = jsonpatch.JsonPatch([{'op': 'test', 'path': '/a', 'value': 1}])
patch2 = jsonpatch.JsonPatch([{'op': 'test', 'path': '/a', 'value': True}])
self.assertNotEqual(patch1, patch2)

def test_patch_equal_numbers_by_value(self):
patch1 = jsonpatch.JsonPatch([{'op': 'test', 'path': '/a', 'value': [1]}])
patch2 = jsonpatch.JsonPatch([{'op': 'test', 'path': '/a', 'value': [1.0]}])
self.assertEqual(patch1, patch2)

def test_patch_hash_equality(self):
patch1 = jsonpatch.JsonPatch([{ "op": "add", "path": "/a/b/c", "value": "foo" }])
patch2 = jsonpatch.JsonPatch([{ "path": "/a/b/c", "op": "add", "value": "foo" }])
Expand Down
Loading