From d4fb742094dba99cb0db17f3aa9d9f5159af676f Mon Sep 17 00:00:00 2001
From: Simon Charette <charettes@users.noreply.github.com>
Date: Wed, 18 Oct 2017 21:43:53 -0400
Subject: [PATCH] Refs #28575 -- Made RelatedObjectDoesNotExist classes
 pickable.

Thanks to Rachel Tobin for the initial __qualname__ work and tests.
---
 .../db/models/fields/related_descriptors.py   | 30 ++++++++++++++++---
 tests/queryset_pickle/models.py               |  1 +
 tests/queryset_pickle/tests.py                | 12 ++++++++
 3 files changed, 39 insertions(+), 4 deletions(-)

diff --git a/django/db/models/fields/related_descriptors.py b/django/db/models/fields/related_descriptors.py
index 39d7223d8a..fb3548f22e 100644
--- a/django/db/models/fields/related_descriptors.py
+++ b/django/db/models/fields/related_descriptors.py
@@ -92,8 +92,13 @@ class ForwardManyToOneDescriptor:
         # still be a string model reference.
         return type(
             'RelatedObjectDoesNotExist',
-            (self.field.remote_field.model.DoesNotExist, AttributeError),
-            {}
+            (self.field.remote_field.model.DoesNotExist, AttributeError), {
+                '__module__': self.field.model.__module__,
+                '__qualname__': '%s.%s.RelatedObjectDoesNotExist' % (
+                    self.field.model.__qualname__,
+                    self.field.name,
+                ),
+            }
         )
 
     def is_cached(self, instance):
@@ -244,6 +249,14 @@ class ForwardManyToOneDescriptor:
         if value is not None and not remote_field.multiple:
             remote_field.set_cached_value(value, instance)
 
+    def __reduce__(self):
+        """
+        Pickling should return the instance attached by self.field on the
+        model, not a new copy of that descriptor. Use getattr() to retrieve
+        the instance directly from the model.
+        """
+        return getattr, (self.field.model, self.field.name)
+
 
 class ForwardOneToOneDescriptor(ForwardManyToOneDescriptor):
     """
@@ -317,8 +330,13 @@ class ReverseOneToOneDescriptor:
         # consistency with `ForwardManyToOneDescriptor`.
         return type(
             'RelatedObjectDoesNotExist',
-            (self.related.related_model.DoesNotExist, AttributeError),
-            {}
+            (self.related.related_model.DoesNotExist, AttributeError), {
+                '__module__': self.related.model.__module__,
+                '__qualname__': '%s.%s.RelatedObjectDoesNotExist' % (
+                    self.related.model.__qualname__,
+                    self.related.name,
+                )
+            },
         )
 
     def is_cached(self, instance):
@@ -455,6 +473,10 @@ class ReverseOneToOneDescriptor:
             # instance to avoid an extra SQL query if it's accessed later on.
             self.related.field.set_cached_value(value, instance)
 
+    def __reduce__(self):
+        # Same purpose as ForwardManyToOneDescriptor.__reduce__().
+        return getattr, (self.related.model, self.related.name)
+
 
 class ReverseManyToOneDescriptor:
     """
diff --git a/tests/queryset_pickle/models.py b/tests/queryset_pickle/models.py
index fba65d7a9e..1275ed6f20 100644
--- a/tests/queryset_pickle/models.py
+++ b/tests/queryset_pickle/models.py
@@ -45,6 +45,7 @@ class Happening(models.Model):
     name = models.CharField(blank=True, max_length=100, default="test")
     number1 = models.IntegerField(blank=True, default=standalone_number)
     number2 = models.IntegerField(blank=True, default=Numbers.get_static_number)
+    event = models.OneToOneField(Event, models.CASCADE, null=True)
 
 
 class Container:
diff --git a/tests/queryset_pickle/tests.py b/tests/queryset_pickle/tests.py
index ebefb690df..27f509a9c1 100644
--- a/tests/queryset_pickle/tests.py
+++ b/tests/queryset_pickle/tests.py
@@ -55,6 +55,18 @@ class PickleabilityTestCase(TestCase):
         klass = Event.MultipleObjectsReturned
         self.assertIs(pickle.loads(pickle.dumps(klass)), klass)
 
+    def test_forward_relatedobjectdoesnotexist_class(self):
+        # ForwardManyToOneDescriptor
+        klass = Event.group.RelatedObjectDoesNotExist
+        self.assertIs(pickle.loads(pickle.dumps(klass)), klass)
+        # ForwardOneToOneDescriptor
+        klass = Happening.event.RelatedObjectDoesNotExist
+        self.assertIs(pickle.loads(pickle.dumps(klass)), klass)
+
+    def test_reverse_one_to_one_relatedobjectdoesnotexist_class(self):
+        klass = Event.happening.RelatedObjectDoesNotExist
+        self.assertIs(pickle.loads(pickle.dumps(klass)), klass)
+
     def test_manager_pickle(self):
         pickle.loads(pickle.dumps(Happening.objects))