Skip to content

Commit

Permalink
Field instances are hashable on Python 3
Browse files Browse the repository at this point in the history
They use a defined hashing algorithm that matches what equality does
on all versions of Python. Previously, on Python 2, fields were hashed
based on their identity. This violated the rule that equal objects
should have equal hashes, and now they do. Since having equal hashes
does not imply that the objects are equal, this is not expected to be
a compatibility problem.

Included tests with a dict show that there shouldn't be false matches
even with equal hashes.

Fixes #36
  • Loading branch information
jamadden committed Aug 10, 2018
1 parent 919388c commit 68b5f02
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 7 deletions.
9 changes: 8 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ Changes
4.5.1 (unreleased)
------------------

- Nothing changed yet.
- ``Field`` instances are hashable on Python 3, and use a defined
hashing algorithm that matches what equality does on all versions of
Python. Previously, on Python 2, fields were hashed based on their
identity. This violated the rule that equal objects should have
equal hashes, and now they do. Since having equal hashes does not
imply that the objects are equal, this is not expected to be a
compatibility problem. See `issue 36
<https://github.com/zopefoundation/zope.schema/issues/36>`_.


4.5.0 (2017-07-10)
Expand Down
28 changes: 22 additions & 6 deletions src/zope/schema/_bootstrapfields.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,19 +184,35 @@ def validate(self, value):
except StopValidation:
pass

def __eq__(self, other):
# should be the same type
if type(self) != type(other):
return False

# should have the same properties
def __get_property_names_to_compare(self):
# Return the set of property names to compare, ignoring
# order
names = {} # used as set of property names, ignoring values
for interface in providedBy(self):
names.update(getFields(interface))

# order will be different always, don't compare it
if 'order' in names:
del names['order']
return names

def __hash__(self):
# Equal objects should have equal hashes;
# equal hashes does not imply equal objects.
value = (type(self),) + tuple(self.__get_property_names_to_compare())
return hash(value)

def __eq__(self, other):
# should be the same type
if type(self) != type(other):
return False

# should have the same properties
names = self.__get_property_names_to_compare()
# XXX: What about the property names of the other object? Even
# though it's the same type, it could theoretically have
# another interface that it `alsoProvides`.

for name in names:
if getattr(self, name) != getattr(other, name):
return False
Expand Down
28 changes: 28 additions & 0 deletions src/zope/schema/tests/test__bootstrapfields.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,34 @@ def test_set_hit(self):
field.set(inst, 'AFTER')
self.assertEqual(inst.extant, 'AFTER')

def test_is_hashable(self):
field = self._makeOne()
hash(field) # doesn't raise

def test_equal_instances_have_same_hash(self):
# Equal objects should have equal hashes
field1 = self._makeOne()
field2 = self._makeOne()
self.assertIsNot(field1, field2)
self.assertEqual(field1, field2)
self.assertEqual(hash(field1), hash(field2))

def test_hash_across_unequal_instances(self):
# Hash equality does not imply equal objects.
# Our implementation only considers property names,
# not values. That's OK, a dict still does the right thing.
field1 = self._makeOne(title=u'foo')
field2 = self._makeOne(title=u'bar')
self.assertIsNot(field1, field2)
self.assertNotEqual(field1, field2)
self.assertEqual(hash(field1), hash(field2))

d = {field1: 42}
self.assertIn(field1, d)
self.assertEqual(42, d[field1])
self.assertNotIn(field2, d)
with self.assertRaises(KeyError):
d[field2]

class ContainerTests(unittest.TestCase):

Expand Down

0 comments on commit 68b5f02

Please sign in to comment.