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.
66 lines
1.9 KiB
Markdown
66 lines
1.9 KiB
Markdown
# Make `Delorean` hashable again
|
|
|
|
## Problem
|
|
|
|
`Delorean` instances cannot be put in a `set` or used as `dict` keys:
|
|
|
|
```python
|
|
>>> 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:
|
|
|
|
```python
|
|
>>> 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:
|
|
|
|
```python
|
|
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'`.
|