From 5459795ef224c5c81461c06a95d38390ee91f014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Tue, 21 May 2013 12:57:24 +0300 Subject: [PATCH] Fixed #20289 -- pickling of dynamic models --- django/db/models/base.py | 24 +++++++++++++----- tests/queryset_pickle/models.py | 10 ++++++++ tests/queryset_pickle/tests.py | 43 ++++++++++++++++++++++++++++++++- 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/django/db/models/base.py b/django/db/models/base.py index 7a8eece462..b8b92217e0 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -451,16 +451,18 @@ class Model(six.with_metaclass(ModelBase)): need to do things manually, as they're dynamically created classes and only module-level classes can be pickled by the default path. """ - if not self._deferred: - return super(Model, self).__reduce__() data = self.__dict__ + if not self._deferred: + class_id = self._meta.app_label, self._meta.object_name + return model_unpickle, (class_id, [], simple_class_factory), data defers = [] for field in self._meta.fields: if isinstance(self.__class__.__dict__.get(field.attname), - DeferredAttribute): + DeferredAttribute): defers.append(field.attname) model = self._meta.proxy_for_model - return (model_unpickle, (model, defers), data) + class_id = model._meta.app_label, model._meta.object_name + return (model_unpickle, (class_id, defers, deferred_class_factory), data) def _get_pk_val(self, meta=None): if not meta: @@ -1008,12 +1010,22 @@ def get_absolute_url(opts, func, self, *args, **kwargs): class Empty(object): pass +def simple_class_factory(model, attrs): + """ + Needed for dynamic classes. + """ + return model -def model_unpickle(model, attrs): +def model_unpickle(model_id, attrs, factory): """ Used to unpickle Model subclasses with deferred fields. """ - cls = deferred_class_factory(model, attrs) + if isinstance(model_id, tuple): + model = get_model(*model_id) + else: + # Backwards compat - the model was cached directly in earlier versions. + model = model_id + cls = factory(model, attrs) return cls.__new__(cls) model_unpickle.__safe_for_unpickle__ = True diff --git a/tests/queryset_pickle/models.py b/tests/queryset_pickle/models.py index 4bcfcfbf04..3a8973505c 100644 --- a/tests/queryset_pickle/models.py +++ b/tests/queryset_pickle/models.py @@ -36,3 +36,13 @@ 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 Container(object): + # To test pickling we need a class that isn't defined on module, but + # is still available from app-cache. So, the Container class moves + # SomeModel outside of module level + class SomeModel(models.Model): + somefield = models.IntegerField() + +class M2MModel(models.Model): + groups = models.ManyToManyField(Group) diff --git a/tests/queryset_pickle/tests.py b/tests/queryset_pickle/tests.py index da1bb8cb73..b4b540c80d 100644 --- a/tests/queryset_pickle/tests.py +++ b/tests/queryset_pickle/tests.py @@ -3,9 +3,10 @@ from __future__ import absolute_import import pickle import datetime +from django.db import models from django.test import TestCase -from .models import Group, Event, Happening +from .models import Group, Event, Happening, Container, M2MModel class PickleabilityTestCase(TestCase): @@ -49,3 +50,43 @@ class PickleabilityTestCase(TestCase): # can't just use assertEqual(original, unpickled) self.assertEqual(original.__class__, unpickled.__class__) self.assertEqual(original.args, unpickled.args) + + def test_model_pickle(self): + """ + Test that a model not defined on module level is pickleable. + """ + original = Container.SomeModel(pk=1) + dumped = pickle.dumps(original) + reloaded = pickle.loads(dumped) + self.assertEqual(original, reloaded) + # Also, deferred dynamic model works + Container.SomeModel.objects.create(somefield=1) + original = Container.SomeModel.objects.defer('somefield')[0] + dumped = pickle.dumps(original) + reloaded = pickle.loads(dumped) + self.assertEqual(original, reloaded) + self.assertEqual(original.somefield, reloaded.somefield) + + def test_model_pickle_m2m(self): + """ + Test intentionally the automatically created through model. + """ + m1 = M2MModel.objects.create() + g1 = Group.objects.create(name='foof') + m1.groups.add(g1) + m2m_through = M2MModel._meta.get_field_by_name('groups')[0].rel.through + original = m2m_through.objects.get() + dumped = pickle.dumps(original) + reloaded = pickle.loads(dumped) + self.assertEqual(original, reloaded) + + def test_model_pickle_dynamic(self): + class Meta: + proxy = True + dynclass = type("DynamicEventSubclass", (Event, ), + {'Meta': Meta, '__module__': Event.__module__}) + original = dynclass(pk=1) + dumped = pickle.dumps(original) + reloaded = pickle.loads(dumped) + self.assertEqual(original, reloaded) + self.assertIs(reloaded.__class__, dynclass)