Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Fixed #21563 -- Single related object descriptors should work with `h…

…asattr`.

Thanks to Aymeric Augustin for the review and Trac alias monkut for the report.
  • Loading branch information...
commit 75924cfa6dca95aa1f02e38802df285271dc7c14 1 parent c7c6474
@charettes charettes authored
View
35 django/db/models/fields/related.py
@@ -156,6 +156,16 @@ def __init__(self, related):
self.related = related
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):
return hasattr(instance, self.cache_name)
@@ -200,9 +210,12 @@ def __get__(self, instance, instance_type=None):
setattr(rel_obj, self.related.field.get_cache_name(), instance)
setattr(instance, self.cache_name, rel_obj)
if rel_obj is None:
- raise self.related.model.DoesNotExist("%s has no %s." % (
- instance.__class__.__name__,
- self.related.get_accessor_name()))
+ raise self.RelatedObjectDoesNotExist(
+ "%s has no %s." % (
+ instance.__class__.__name__,
+ self.related.get_accessor_name()
+ )
+ )
else:
return rel_obj
@@ -255,6 +268,17 @@ def __init__(self, field_with_rel):
self.field = field_with_rel
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):
return hasattr(instance, self.cache_name)
@@ -321,8 +345,9 @@ def __get__(self, instance, instance_type=None):
setattr(rel_obj, self.field.related.get_cache_name(), instance)
setattr(instance, self.cache_name, rel_obj)
if rel_obj is None and not self.field.null:
- raise self.field.rel.to.DoesNotExist(
- "%s has no %s." % (self.field.model.__name__, self.field.name))
+ raise self.RelatedObjectDoesNotExist(
+ "%s has no %s." % (self.field.model.__name__, self.field.name)
+ )
else:
return rel_obj
View
4 tests/one_to_one/tests.py
@@ -25,6 +25,10 @@ def test_getter(self):
# p2 doesn't have an associated restaurant.
with self.assertRaisesMessage(Restaurant.DoesNotExist, 'Place has no 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):
# Set the place using assignment notation. Because place is the primary
View
13 tests/reverse_single_related/tests.py
@@ -33,5 +33,14 @@ def test_reverse_single_related(self):
# 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.
Source.objects.use_for_related_fields = True
- private_item = Item.objects.get(pk=private_item.pk)
- self.assertRaises(Source.DoesNotExist, lambda: private_item.source)
+ try:
+ 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'))
Please sign in to comment.
Something went wrong with that request. Please try again.