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
30 changes: 29 additions & 1 deletion jsonpatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,34 @@ class JsonPatchTestFailed(JsonPatchException, AssertionError):
""" A Test operation failed """


def _compare_json_values(left, right):
"""Compare two JSON values for equality as required by RFC 6902.

Two values are only considered equal if they are of the same JSON
type. This needs special handling because in Python ``bool`` is a
subclass of ``int`` (so ``True == 1`` and ``False == 0``), whereas in
JSON the ``true``/``false`` literals are a distinct type from numbers.
Containers are compared recursively so the same rule applies to values
nested inside arrays and objects.
"""
if isinstance(left, bool) or isinstance(right, bool):
return isinstance(left, bool) and isinstance(right, bool) \
and left == right

if isinstance(left, MutableMapping) and isinstance(right, MutableMapping):
return left.keys() == right.keys() and all(
_compare_json_values(left[key], right[key]) for key in left
)

if isinstance(left, MutableSequence) and isinstance(right, MutableSequence):
return len(left) == len(right) and all(
_compare_json_values(lval, rval)
for lval, rval in zip(left, right)
)

return left == right


def multidict(ordered_pairs):
"""Convert duplicate keys values to lists."""
# read all values into lists
Expand Down Expand Up @@ -468,7 +496,7 @@ def apply(self, obj):
raise InvalidJsonPatch(
"The operation does not contain a 'value' member")

if val != value:
if not _compare_json_values(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
35 changes: 35 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,41 @@ def test_test_noval_not_existing_nested(self):
jsonpatch.apply_patch,
obj, [{'op': 'test', 'path': '/baz/qx'}])

def test_test_bool_and_int_not_equal(self):
# RFC 6902 requires the tested value to be of the same JSON type,
# so the number 1 must not test-equal the boolean true (in Python
# True == 1), and likewise 0 must not test-equal false.
self.assertRaises(jsonpatch.JsonPatchTestFailed,
jsonpatch.apply_patch,
{'baz': 1}, [{'op': 'test', 'path': '/baz', 'value': True}])
self.assertRaises(jsonpatch.JsonPatchTestFailed,
jsonpatch.apply_patch,
{'baz': True}, [{'op': 'test', 'path': '/baz', 'value': 1}])
self.assertRaises(jsonpatch.JsonPatchTestFailed,
jsonpatch.apply_patch,
{'baz': 0}, [{'op': 'test', 'path': '/baz', 'value': False}])

def test_test_bool_matches_bool(self):
# Same-typed boolean values still test-equal.
obj = {'baz': True}
res = jsonpatch.apply_patch(obj, [{'op': 'test', 'path': '/baz', 'value': True}])
self.assertEqual(res, obj)

def test_test_int_and_float_equal(self):
# Numbers are compared numerically, so 1 and 1.0 test-equal.
obj = {'baz': 1}
res = jsonpatch.apply_patch(obj, [{'op': 'test', 'path': '/baz', 'value': 1.0}])
self.assertEqual(res, obj)

def test_test_bool_and_int_nested_not_equal(self):
# The same-type rule also applies to values nested in containers.
self.assertRaises(jsonpatch.JsonPatchTestFailed,
jsonpatch.apply_patch,
{'baz': [1]}, [{'op': 'test', 'path': '/baz', 'value': [True]}])
self.assertRaises(jsonpatch.JsonPatchTestFailed,
jsonpatch.apply_patch,
{'baz': {'q': 1}}, [{'op': 'test', 'path': '/baz', 'value': {'q': True}}])


def test_unrecognized_element(self):
obj = {'foo': 'bar', 'baz': 'qux'}
Expand Down