Defining __eq__ without __hash__ sets __hash__ to None in Python 3, so Delorean instances were unhashable and could not be used in sets or as dict keys (unlike datetime). Hash on self.epoch, the same value __eq__ compares, so equal instances hash equal.
1.9 KiB
Make Delorean hashable again
Problem
Delorean instances cannot be put in a set or used as dict keys:
>>> from datetime import datetime
>>> from delorean import Delorean
>>> d = Delorean(datetime(2015, 1, 1), timezone="UTC")
>>> hash(d)
Traceback (most recent call last):
...
TypeError: unhashable type: 'Delorean'
>>> {d}
TypeError: unhashable type: 'Delorean'
This is surprising for a date/time value object: the standard library's
datetime.datetime is hashable, and Delorean supports equality and ordering,
so grouping instances in sets or keying dictionaries by them is a natural thing
to want.
Root cause
delorean/dates.py defines __eq__ (and the rich-comparison operators) on
Delorean, but does not define __hash__. In Python 3, defining __eq__
without __hash__ sets __hash__ to None, which makes the class unhashable:
>>> Delorean.__hash__ is None
True
Under Python 2 (the version delorean originally targeted) defining __eq__
left the inherited __hash__ in place, so this is effectively a regression
introduced by the move to Python 3.
Fix
Add a __hash__ consistent with the existing __eq__. __eq__ compares
self.epoch == other.epoch, so hashing on self.epoch guarantees the hash
invariant (equal objects hash equal), including two instances describing the
same instant in different timezones:
def __hash__(self):
return hash(self.epoch)
Test
Added two tests to tests/delorean_tests.py:
test_hashable:hash()works and an instance can be stored in a set and used as a dict key.test_hash_consistent_with_equality: two equal instances (same instant, different timezone) hash equal and collapse to one element in a set.
Gate
python -m pytest tests/-> 232 passed, 44 subtests passed.
Counterfactual (fix reverted, tests kept): both new tests fail with
TypeError: unhashable type: 'Delorean'.