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
8 changes: 4 additions & 4 deletions jsonpatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -1459,13 +1459,13 @@ def _json_equal(first, second):
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):
if isinstance(first, Mapping) and isinstance(second, Mapping):
return len(first) == len(second) and all(
key in second and _json_equal(first[key], second[key])
for key in first)
Comment on lines 1459 to 1465

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 51b32f1. The equality check now accepts non-text Sequence implementations, consistent with the stubs and the immutable-container handling in _restore; strings and bytes stay scalar, and write operations still require mutable containers.

The new regressions cover tuple/list comparisons in both directions, a generic read-only sequence, nested containers, Boolean/number distinctions, numeric equivalence, and unchanged inputs. Of the 95 new subcases, 33 fail on the previous PR revision. The updated suite passes locally: 322 unit tests (one skipped) and 26 standard-profile property tests.

if isinstance(first, MutableSequence) and \
isinstance(second, MutableSequence):
if isinstance(first, Sequence) and isinstance(second, Sequence) and \
not isinstance(first, (str, bytes)) and \
not isinstance(second, (str, bytes)):
return len(first) == len(second) and all(
_json_equal(*items) for items in zip(first, second))
return first == second
Expand Down
183 changes: 182 additions & 1 deletion tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import stat
import subprocess
import tempfile
from collections.abc import Mapping
from collections.abc import Mapping, Sequence
from types import MappingProxyType
from unittest import mock

Expand Down Expand Up @@ -446,6 +446,187 @@ def test_test_literals_differ_from_numbers(self):
jsonpatch.apply_patch(
doc, [{'op': 'test', 'path': '/a', 'value': value}])

def test_test_read_only_mapping_preserves_boolean_types(self):
for mapping_type in (dict, MappingProxyType):
for nested in (False, True):
for actual, expected in ((True, 1), (False, 0),
(1, True), (0, False)):
with self.subTest(mapping_type=mapping_type, nested=nested,
actual=actual, expected=expected):
mapping = mapping_type({'a': actual})
doc = {'nested': mapping} if nested else mapping
path = '/nested' if nested else ''
original = json.dumps(doc, default=dict)
patch = [{'op': 'test', 'path': path,
'value': {'a': expected}}]

with self.assertRaises(jsonpatch.JsonPatchTestFailed):
jsonpatch.apply_patch(doc, patch, in_place=True)

self.assertEqual(json.dumps(doc, default=dict), original)
Comment on lines +449 to +466
if nested:
self.assertIs(doc['nested'], mapping)

def test_test_nested_read_only_mapping_preserves_boolean_types(self):
documents = [
({'nested': MappingProxyType({'a': True})},
{'nested': {'a': 1}}),
([MappingProxyType({'a': False})], [{'a': 0}]),
(MappingProxyType({'nested': [True]}), {'nested': [1]}),
]
for doc, value in documents:
with self.subTest(doc=doc, value=value):
original = json.dumps(doc, default=dict)
with self.assertRaises(jsonpatch.JsonPatchTestFailed):
jsonpatch.apply_patch(
doc, [{'op': 'test', 'path': '', 'value': value}],
in_place=True)
self.assertEqual(json.dumps(doc, default=dict), original)

def test_test_read_only_mapping_success_preserves_input(self):
for mapping_type in (dict, MappingProxyType):
for nested in (False, True):
for actual, expected in ((1, 1.0), (1.0, 1), (True, True),
(False, False), (None, None)):
with self.subTest(mapping_type=mapping_type, nested=nested,
actual=actual, expected=expected):
mapping = mapping_type({'a': actual})
doc = {'nested': mapping} if nested else mapping
path = '/nested' if nested else ''
original = json.dumps(doc, default=dict)
result = jsonpatch.apply_patch(
doc, [{'op': 'test', 'path': path,
'value': {'a': expected}}], in_place=True)

self.assertIs(result, doc)
self.assertEqual(json.dumps(doc, default=dict), original)
if nested:
self.assertIs(result['nested'], mapping)


def test_test_read_only_sequences_preserve_boolean_types(self):
for actual_type in (list, tuple):
for expected_type in (list, tuple):
for nested in (False, True):
for actual, expected in ((True, 1), (False, 0),
(1, True), (0, False)):
with self.subTest(actual_type=actual_type,
expected_type=expected_type,
nested=nested, actual=actual,
expected=expected):
sequence = actual_type([actual])
doc = {'nested': sequence} if nested else sequence
path = '/nested' if nested else ''
original = json.dumps(doc)
patch = [{'op': 'test', 'path': path,
'value': expected_type([expected])}]

with self.assertRaises(jsonpatch.JsonPatchTestFailed):
jsonpatch.apply_patch(doc, patch, in_place=True)

self.assertEqual(json.dumps(doc), original)
if nested:
self.assertIs(doc['nested'], sequence)

def test_test_read_only_sequence_success_preserves_input(self):
for actual_type in (list, tuple):
for expected_type in (list, tuple):
for nested in (False, True):
for actual, expected in ((1, 1.0), (1.0, 1), (True, True),
(False, False), (None, None)):
with self.subTest(actual_type=actual_type,
expected_type=expected_type,
nested=nested, actual=actual,
expected=expected):
sequence = actual_type([actual])
doc = {'nested': sequence} if nested else sequence
path = '/nested' if nested else ''
original = json.dumps(doc)
patch = [{'op': 'test', 'path': path,
'value': expected_type([expected])}]

result = jsonpatch.apply_patch(doc, patch, in_place=True)

self.assertIs(result, doc)
self.assertEqual(json.dumps(doc), original)
if nested:
self.assertIs(doc['nested'], sequence)

def test_test_custom_read_only_sequences(self):
class FrozenSequence(Sequence):
def __init__(self, values):
self.values = tuple(values)

def __getitem__(self, index):
return self.values[index]

def __len__(self):
return len(self.values)

def snapshot(value):
return json.dumps(value, default=lambda value:
dict(value) if isinstance(value, Mapping)
else list(value))

cases = [
(FrozenSequence([True]), [1], False),
([True], FrozenSequence([1]), False),
(FrozenSequence([1]), [1.0], True),
([True], FrozenSequence([True]), True),
(FrozenSequence([]), (), True),
(FrozenSequence([{'a': (True,)}]), [{'a': [True]}], True),
(FrozenSequence([MappingProxyType({'a': (True,)})]),
[{'a': [1]}], False),
([{'a': FrozenSequence([False])}], ({'a': (0,)},), False),
]
for doc, value, succeeds in cases:
with self.subTest(doc=doc, value=value):
original, original_value = snapshot(doc), snapshot(value)
patch = [{'op': 'test', 'path': '', 'value': value}]
if succeeds:
self.assertIs(jsonpatch.apply_patch(
doc, patch, in_place=True), doc)
else:
with self.assertRaises(jsonpatch.JsonPatchTestFailed):
jsonpatch.apply_patch(doc, patch, in_place=True)
self.assertEqual(snapshot(doc), original)
self.assertEqual(snapshot(value), original_value)

def test_test_read_only_sequence_contents_and_lengths(self):
for doc, value in [
((True,), (1,)),
(({'a': (False,)},), [{'a': [0]}]),
((), [1]),
((1,), [1, 2]),
((1, 2), [2, 1]),
]:
with self.subTest(doc=doc, value=value):
with self.assertRaises(jsonpatch.JsonPatchTestFailed):
jsonpatch.apply_patch(
doc, [{'op': 'test', 'path': '', 'value': value}],
in_place=True)

def test_test_strings_and_bytes_remain_scalar(self):
for doc, value, succeeds in [
('', '', True),
('text', 'text', True),
(b'', b'', True),
(b'bytes', b'bytes', True),
('text', 'next', False),
(b'bytes', b'other', False),
('text', ('t', 'e', 'x', 't'), False),
(('t', 'e', 'x', 't'), 'text', False),
(b'12', (49, 50), False),
((49, 50), b'12', False),
]:
with self.subTest(doc=doc, value=value):
patch = [{'op': 'test', 'path': '', 'value': value}]
if succeeds:
self.assertIs(jsonpatch.apply_patch(
doc, patch, in_place=True), doc)
else:
with self.assertRaises(jsonpatch.JsonPatchTestFailed):
jsonpatch.apply_patch(doc, patch, in_place=True)

def test_test_not_existing(self):
obj = {'bar': 'qux'}
Expand Down