mirror of
https://github.com/django/django.git
synced 2025-06-05 03:29:12 +00:00
Fixed model.__eq__ and __hash__ for no pk value cases
The __eq__ method now considers two instances without primary key value equal only when they have same id(). The __hash__ method raises TypeError for no primary key case. Fixed #18864, fixed #18250 Thanks to Tim Graham for docs review.
This commit is contained in:
parent
768bbf3efe
commit
6af05e7a0f
@ -459,14 +459,21 @@ class Model(six.with_metaclass(ModelBase)):
|
|||||||
return '%s object' % self.__class__.__name__
|
return '%s object' % self.__class__.__name__
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return (isinstance(other, Model) and
|
if not isinstance(other, Model):
|
||||||
self._meta.concrete_model == other._meta.concrete_model and
|
return False
|
||||||
self._get_pk_val() == other._get_pk_val())
|
if self._meta.concrete_model != other._meta.concrete_model:
|
||||||
|
return False
|
||||||
|
my_pk = self._get_pk_val()
|
||||||
|
if my_pk is None:
|
||||||
|
return self is other
|
||||||
|
return my_pk == other._get_pk_val()
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
|
if self._get_pk_val() is None:
|
||||||
|
raise TypeError("Model instances without primary key value are unhashable")
|
||||||
return hash(self._get_pk_val())
|
return hash(self._get_pk_val())
|
||||||
|
|
||||||
def __reduce__(self):
|
def __reduce__(self):
|
||||||
|
@ -631,7 +631,11 @@ class BaseModelFormSet(BaseFormSet):
|
|||||||
seen_data = set()
|
seen_data = set()
|
||||||
for form in valid_forms:
|
for form in valid_forms:
|
||||||
# get data for each field of each of unique_check
|
# get data for each field of each of unique_check
|
||||||
row_data = tuple([form.cleaned_data[field] for field in unique_check if field in form.cleaned_data])
|
row_data = (form.cleaned_data[field]
|
||||||
|
for field in unique_check if field in form.cleaned_data)
|
||||||
|
# Reduce Model instances to their primary key values
|
||||||
|
row_data = tuple(d._get_pk_val() if hasattr(d, '_get_pk_val') else d
|
||||||
|
for d in row_data)
|
||||||
if row_data and not None in row_data:
|
if row_data and not None in row_data:
|
||||||
# if we've already seen it then we have a uniqueness failure
|
# if we've already seen it then we have a uniqueness failure
|
||||||
if row_data in seen_data:
|
if row_data in seen_data:
|
||||||
|
@ -521,6 +521,25 @@ For example::
|
|||||||
In previous versions only instances of the exact same class and same
|
In previous versions only instances of the exact same class and same
|
||||||
primary key value were considered equal.
|
primary key value were considered equal.
|
||||||
|
|
||||||
|
``__hash__``
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. method:: Model.__hash__()
|
||||||
|
|
||||||
|
The ``__hash__`` method is based on the instance's primary key value. It
|
||||||
|
is effectively hash(obj.pk). If the instance doesn't have a primary key
|
||||||
|
value then a ``TypeError`` will be raised (otherwise the ``__hash__``
|
||||||
|
method would return different values before and after the instance is
|
||||||
|
saved, but changing the ``__hash__`` value of an instance `is forbidden
|
||||||
|
in Python`_).
|
||||||
|
|
||||||
|
.. versionchanged:: 1.7
|
||||||
|
|
||||||
|
In previous versions instance's without primary key value were
|
||||||
|
hashable.
|
||||||
|
|
||||||
|
.. _is forbidden in Python: http://docs.python.org/reference/datamodel.html#object.__hash__
|
||||||
|
|
||||||
``get_absolute_url``
|
``get_absolute_url``
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
@ -199,6 +199,14 @@ Miscellaneous
|
|||||||
equal when primary keys match. Previously only instances of exact same
|
equal when primary keys match. Previously only instances of exact same
|
||||||
class were considered equal on primary key match.
|
class were considered equal on primary key match.
|
||||||
|
|
||||||
|
* The :meth:`django.db.models.Model.__eq__` method has changed such that
|
||||||
|
two ``Model`` instances without primary key values won't be considered
|
||||||
|
equal (unless they are the same instance).
|
||||||
|
|
||||||
|
* The :meth:`django.db.models.Model.__hash__` will now raise ``TypeError``
|
||||||
|
when called on an instance without a primary key value. This is done to
|
||||||
|
avoid mutable ``__hash__`` values in containers.
|
||||||
|
|
||||||
Features deprecated in 1.7
|
Features deprecated in 1.7
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
|
@ -708,9 +708,20 @@ class ModelTest(TestCase):
|
|||||||
SelfRef.objects.get(selfref=sr)
|
SelfRef.objects.get(selfref=sr)
|
||||||
|
|
||||||
def test_eq(self):
|
def test_eq(self):
|
||||||
|
self.assertEqual(Article(id=1), Article(id=1))
|
||||||
self.assertNotEqual(Article(id=1), object())
|
self.assertNotEqual(Article(id=1), object())
|
||||||
self.assertNotEqual(object(), Article(id=1))
|
self.assertNotEqual(object(), Article(id=1))
|
||||||
|
a = Article()
|
||||||
|
self.assertEqual(a, a)
|
||||||
|
self.assertNotEqual(Article(), a)
|
||||||
|
|
||||||
|
def test_hash(self):
|
||||||
|
# Value based on PK
|
||||||
|
self.assertEqual(hash(Article(id=1)), hash(1))
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
# No PK value -> unhashable (because save() would then change
|
||||||
|
# hash)
|
||||||
|
hash(Article())
|
||||||
|
|
||||||
class ConcurrentSaveTests(TransactionTestCase):
|
class ConcurrentSaveTests(TransactionTestCase):
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user