Reputation: 1048
I'm testing the update of an application from Django 2.1.7 to 2.2.12. I got an error when running my unit tests, which boils down to a model object not being hashable :
Station.objects.all().delete()
py37\lib\site-packages\django\db\models\query.py:710: in delete
collector.collect(del_query)
py37\lib\site-packages\django\db\models\deletion.py:192: in collect
reverse_dependency=reverse_dependency)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <django.db.models.deletion.Collector object at 0x000001EC78243E80>
objs = <QuerySet [<Station(nom='DUNKERQUE')>, <Station(nom='STATION1')>, <Station(nom='STATION2')>]>, source = None, nullable = False
reverse_dependency = False
def add(self, objs, source=None, nullable=False, reverse_dependency=False):
"""
Add 'objs' to the collection of objects to be deleted. If the call is
the result of a cascade, 'source' should be the model that caused it,
and 'nullable' should be set to True if the relation can be null.
Return a list of all objects that were not already collected.
"""
if not objs:
return []
new_objs = []
model = objs[0].__class__
instances = self.data.setdefault(model, set())
for obj in objs:
> if obj not in instances:
E TypeError: unhashable type: 'Station'
Instances of model objects are hashable in Django, once they are saved to database and get a primary key.
I don't understand where the error comes from and why I get this when running this basic code:
In [7]: s = Station.objects.create(nom='SOME PLACE')
In [8]: hash(s)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-8-9333020f3184> in <module>
----> 1 hash(s)
TypeError: unhashable type: 'Station'
In [9]: s.pk
Out[9]: 2035
All this code works fine when I switch back to Django 2.1.7. The same happens with other model objects in the app. I'm using python version 3.7.2 on Windows, with a SQlite backend (on the development workstation).
Edit: Here's the definition of the model referred to above:
class Station(models.Model):
nom = models.CharField(max_length=200, unique=True)
def __str__(self):
return self.nom
def __repr__(self):
return "<Station(nom='{}')>".format(self.nom)
def __eq__(self, other):
return isinstance(other, Station) and self.nom == other.nom
Upvotes: 7
Views: 2230
Reputation: 1048
As pointed out by @Alasdair, the issue was a change of behaviour brought in Django 2.2 to comply with how a model class should behave when __eq__()
is overriden but not __hash__()
. As per the python docs for __hash__()
:
A class that overrides
__eq__()
and does not define__hash__()
will have its__hash__()
implicitly set to None.
More information about the implementation of this behaviour in Django can be found in this ticket.
The fix can be either the one suggested in the ticket, i.e. re-assigning the __hash__()
method of the model to the one of the super class:
__hash__ = models.Model.__hash__
Or a more object-oriented way could be:
def __hash__(self):
return super().__hash__()
This seems a bit weird because this should be unnecessary: by default, a call to __hash__()
should use the method from the super class where it's implemented. This suggests Django breaks encapsulation somehow. But maybe I don't understand everything. Anyway that's a sidenote.
In my case, I still wanted to be able to compare model instances not yet saved to the database for testing purposes and ended up with this implementation :
def __hash__(self):
if self.pk is None:
return hash(self.nom)
return super().__hash__()
Upvotes: 19