Fixed #21563 -- Single related object descriptors should work with `hasattr`.
Thanks to Aymeric Augustin for the review and Trac alias monkut for the report.
This commit is contained in:
parent
c7c647419c
commit
75924cfa6d
|
@ -156,6 +156,16 @@ class SingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjectDescri
|
||||||
self.related = related
|
self.related = related
|
||||||
self.cache_name = related.get_cache_name()
|
self.cache_name = related.get_cache_name()
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def RelatedObjectDoesNotExist(self):
|
||||||
|
# The exception isn't created at initialization time for the sake of
|
||||||
|
# consistency with `ReverseSingleRelatedObjectDescriptor`.
|
||||||
|
return type(
|
||||||
|
str('RelatedObjectDoesNotExist'),
|
||||||
|
(self.related.model.DoesNotExist, AttributeError),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
def is_cached(self, instance):
|
def is_cached(self, instance):
|
||||||
return hasattr(instance, self.cache_name)
|
return hasattr(instance, self.cache_name)
|
||||||
|
|
||||||
|
@ -200,9 +210,12 @@ class SingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjectDescri
|
||||||
setattr(rel_obj, self.related.field.get_cache_name(), instance)
|
setattr(rel_obj, self.related.field.get_cache_name(), instance)
|
||||||
setattr(instance, self.cache_name, rel_obj)
|
setattr(instance, self.cache_name, rel_obj)
|
||||||
if rel_obj is None:
|
if rel_obj is None:
|
||||||
raise self.related.model.DoesNotExist("%s has no %s." % (
|
raise self.RelatedObjectDoesNotExist(
|
||||||
instance.__class__.__name__,
|
"%s has no %s." % (
|
||||||
self.related.get_accessor_name()))
|
instance.__class__.__name__,
|
||||||
|
self.related.get_accessor_name()
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return rel_obj
|
return rel_obj
|
||||||
|
|
||||||
|
@ -255,6 +268,17 @@ class ReverseSingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjec
|
||||||
self.field = field_with_rel
|
self.field = field_with_rel
|
||||||
self.cache_name = self.field.get_cache_name()
|
self.cache_name = self.field.get_cache_name()
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def RelatedObjectDoesNotExist(self):
|
||||||
|
# The exception can't be created at initialization time since the
|
||||||
|
# related model might not be resolved yet; `rel.to` might still be
|
||||||
|
# a string model reference.
|
||||||
|
return type(
|
||||||
|
str('RelatedObjectDoesNotExist'),
|
||||||
|
(self.field.rel.to.DoesNotExist, AttributeError),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
def is_cached(self, instance):
|
def is_cached(self, instance):
|
||||||
return hasattr(instance, self.cache_name)
|
return hasattr(instance, self.cache_name)
|
||||||
|
|
||||||
|
@ -321,8 +345,9 @@ class ReverseSingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjec
|
||||||
setattr(rel_obj, self.field.related.get_cache_name(), instance)
|
setattr(rel_obj, self.field.related.get_cache_name(), instance)
|
||||||
setattr(instance, self.cache_name, rel_obj)
|
setattr(instance, self.cache_name, rel_obj)
|
||||||
if rel_obj is None and not self.field.null:
|
if rel_obj is None and not self.field.null:
|
||||||
raise self.field.rel.to.DoesNotExist(
|
raise self.RelatedObjectDoesNotExist(
|
||||||
"%s has no %s." % (self.field.model.__name__, self.field.name))
|
"%s has no %s." % (self.field.model.__name__, self.field.name)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return rel_obj
|
return rel_obj
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,10 @@ class OneToOneTests(TestCase):
|
||||||
# p2 doesn't have an associated restaurant.
|
# p2 doesn't have an associated restaurant.
|
||||||
with self.assertRaisesMessage(Restaurant.DoesNotExist, 'Place has no restaurant'):
|
with self.assertRaisesMessage(Restaurant.DoesNotExist, 'Place has no restaurant'):
|
||||||
self.p2.restaurant
|
self.p2.restaurant
|
||||||
|
# The exception raised on attribute access when a related object
|
||||||
|
# doesn't exist should be an instance of a subclass of `AttributeError`
|
||||||
|
# refs #21563
|
||||||
|
self.assertFalse(hasattr(self.p2, 'restaurant'))
|
||||||
|
|
||||||
def test_setter(self):
|
def test_setter(self):
|
||||||
# Set the place using assignment notation. Because place is the primary
|
# Set the place using assignment notation. Because place is the primary
|
||||||
|
|
|
@ -33,5 +33,14 @@ class ReverseSingleRelatedTests(TestCase):
|
||||||
# of the "bare" queryset. Usually you'd define this as a property on the class,
|
# of the "bare" queryset. Usually you'd define this as a property on the class,
|
||||||
# but this approximates that in a way that's easier in tests.
|
# but this approximates that in a way that's easier in tests.
|
||||||
Source.objects.use_for_related_fields = True
|
Source.objects.use_for_related_fields = True
|
||||||
private_item = Item.objects.get(pk=private_item.pk)
|
try:
|
||||||
self.assertRaises(Source.DoesNotExist, lambda: private_item.source)
|
private_item = Item.objects.get(pk=private_item.pk)
|
||||||
|
self.assertRaises(Source.DoesNotExist, lambda: private_item.source)
|
||||||
|
finally:
|
||||||
|
Source.objects.use_for_related_fields = False
|
||||||
|
|
||||||
|
def test_hasattr_single_related(self):
|
||||||
|
# The exception raised on attribute access when a related object
|
||||||
|
# doesn't exist should be an instance of a subclass of `AttributeError`
|
||||||
|
# refs #21563
|
||||||
|
self.assertFalse(hasattr(Item(), 'source'))
|
||||||
|
|
Loading…
Reference in New Issue