diff --git a/jsonpatch.py b/jsonpatch.py index 5383aec..47e99a0 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -190,7 +190,9 @@ def apply(self, obj): raise NotImplementedError('should implement the patch operation.') def __hash__(self): - return hash(frozenset(self.operation.items())) + # 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'])) def __eq__(self, other): if not isinstance(other, PatchOperation): diff --git a/property_tests.py b/property_tests.py index 9e92ebf..bbd086c 100644 --- a/property_tests.py +++ b/property_tests.py @@ -450,8 +450,6 @@ def test_string_roundtrip(self, operations): self.assertEqual(jsonpatch.JsonPatch.from_string(patch.to_string()), patch) - # hashing fails for operations whose value is an array or object - @unittest.expectedFailure @given(patches) @example(operations=[{'op': 'add', 'path': '/a', 'value': []}]) def test_equal_patches_have_equal_hashes(self, operations): diff --git a/tests.py b/tests.py index af42fbc..9f6e8d0 100755 --- a/tests.py +++ b/tests.py @@ -375,6 +375,20 @@ def test_patch_hash_unequal(self): self.assertNotEqual(hash(patch1), hash(patch2)) + def test_patch_hash_container_values(self): + for value in ([], {}, [1, {'b': []}], {'b': [1]}): + patch1 = jsonpatch.JsonPatch([{'op': 'add', 'path': '/a', 'value': value}]) + patch2 = jsonpatch.JsonPatch([{'op': 'add', 'path': '/a', 'value': copy.deepcopy(value)}]) + self.assertEqual(hash(patch1), hash(patch2)) + self.assertEqual(len({patch1, patch2}), 1) + + + def test_patch_hash_ignored_container_member(self): + # members an operation doesn't use are ignored, whatever their value + patch = jsonpatch.JsonPatch([{'op': 'add', 'path': '/a', 'value': 1, 'from': []}]) + self.assertIn(patch, {patch}) + + def test_patch_neq_other_objs(self): p = [{'op': 'test', 'path': '/test'}] patch = jsonpatch.JsonPatch(p)