From d4fb742094dba99cb0db17f3aa9d9f5159af676f Mon Sep 17 00:00:00 2001 From: Simon Charette 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))