Commit bac187c0 authored by Anssi Kääriäinen's avatar Anssi Kääriäinen
Browse files

[1.5.x] Fixed prefetch_related + pickle regressions

There were a couple of regressions related to field pickling. The
regressions were introduced by QuerySet._known_related_objects caching.

The regressions aren't present in master, the fix was likely in
f403653c.

Fixed #20157, fixed #20257. Also made QuerySets with model=None
picklable.
parent 63cab03f
Loading
Loading
Loading
Loading
+17 −1
Original line number Diff line number Diff line
@@ -68,11 +68,27 @@ class QuerySet(object):
        """
        # Force the cache to be fully populated.
        len(self)

        obj_dict = self.__dict__.copy()
        obj_dict['_iter'] = None
        obj_dict['_known_related_objects'] = dict(
            (field.name, val) for field, val in self._known_related_objects.items()
        )
        return obj_dict

    def __setstate__(self, obj_dict):
        model = obj_dict['model']
        if model is None:
            # if model is None, then self should be emptyqs and the related
            # objects do not matter.
            self._known_related_objects = {}
        else:
            opts = model._meta
            self._known_related_objects = dict(
                (opts.get_field(field.name if hasattr(field, 'name') else field), val)
                for field, val in obj_dict['_known_related_objects'].items()
            )
        self.__dict__.update(obj_dict)

    def __repr__(self):
        data = list(self[:REPR_OUTPUT_SIZE + 1])
        if len(data) > REPR_OUTPUT_SIZE:
+11 −6
Original line number Diff line number Diff line
@@ -208,12 +208,17 @@ class Query(object):
        Unpickling support.
        """
        # Rebuild list of field instances
        opts = obj_dict['model']._meta
        model = obj_dict['model']
        if model is None:
            # if model is None the queryset should be emptyqs. So the
            # select_fields do not matter.
            obj_dict['select_fields'] = []
        else:
            opts = model._meta
            obj_dict['select_fields'] = [
                name is not None and opts.get_field(name) or None
                for name in obj_dict['select_fields']
            ]

        self.__dict__.update(obj_dict)

    def prepare(self):
+13 −0
Original line number Diff line number Diff line
@@ -36,3 +36,16 @@ class Happening(models.Model):
    number2 = models.IntegerField(blank=True, default=Numbers.get_static_number)
    number3 = models.IntegerField(blank=True, default=Numbers.get_class_number)
    number4 = models.IntegerField(blank=True, default=nn.get_member_number)

class Person(models.Model):
    name = models.CharField(max_length=200)

class SocialProfile(models.Model):
    person = models.ForeignKey(Person)
    friends = models.ManyToManyField('self')

class Post(models.Model):
    post_date = models.DateTimeField(default=datetime.datetime.now)

class Material(models.Model):
    post = models.ForeignKey(Post, related_name='materials')
+28 −1
Original line number Diff line number Diff line
@@ -5,7 +5,8 @@ import datetime

from django.test import TestCase

from .models import Group, Event, Happening
from .models import Group, Event, Happening, Person, Post
from django.contrib.auth.models import AnonymousUser


class PickleabilityTestCase(TestCase):
@@ -46,3 +47,29 @@ class PickleabilityTestCase(TestCase):
        # can't just use assertEqual(original, unpickled)
        self.assertEqual(original.__class__, unpickled.__class__)
        self.assertEqual(original.args, unpickled.args)

    def test_pickle_m2m_prefetch_related(self):
        bob = Person(name="Bob")
        bob.save()
        people = Person.objects.prefetch_related('socialprofile_set')
        dumped = pickle.dumps(people)
        people = pickle.loads(dumped)
        self.assertQuerysetEqual(
            people, [bob], lambda x: x)

    def test_pickle_field_default_prefetch_related(self):
        p1 = Post.objects.create()
        posts = Post.objects.prefetch_related('materials')
        dumped = pickle.dumps(posts)
        posts = pickle.loads(dumped)
        self.assertQuerysetEqual(
            posts, [p1], lambda x: x)

    def test_pickle_emptyqs(self):
        u = AnonymousUser()
        # Use AnonymousUser, as AnonymousUser.groups has qs.model = None
        empty = u.groups.all()
        dumped = pickle.dumps(empty)
        empty = pickle.loads(dumped)
        self.assertQuerysetEqual(
            empty, [])