Explorar el Código

Fixed #21563 -- Single related object descriptors should work with `hasattr`.

Thanks to Aymeric Augustin for the review and Trac alias monkut for the report.
Simon Charette hace 11 años
padre
commit
75924cfa6d

+ 30 - 5
django/db/models/fields/related.py

@@ -156,6 +156,16 @@ class SingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjectDescri
         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 @@ class SingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjectDescri
                     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 @@ class ReverseSingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjec
         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 @@ class ReverseSingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjec
                     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
 

+ 4 - 0
tests/one_to_one/tests.py

@@ -25,6 +25,10 @@ class OneToOneTests(TestCase):
         # 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

+ 11 - 2
tests/reverse_single_related/tests.py

@@ -33,5 +33,14 @@ class ReverseSingleRelatedTests(TestCase):
         # 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'))