Fixed #20289 -- pickling of dynamic models

This commit is contained in:
Anssi Kääriäinen 2013-05-21 12:57:24 +03:00
parent 855d1305c5
commit 5459795ef2
3 changed files with 70 additions and 7 deletions

View File

@ -451,16 +451,18 @@ class Model(six.with_metaclass(ModelBase)):
need to do things manually, as they're dynamically created classes and need to do things manually, as they're dynamically created classes and
only module-level classes can be pickled by the default path. only module-level classes can be pickled by the default path.
""" """
if not self._deferred:
return super(Model, self).__reduce__()
data = self.__dict__ 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 = [] defers = []
for field in self._meta.fields: for field in self._meta.fields:
if isinstance(self.__class__.__dict__.get(field.attname), if isinstance(self.__class__.__dict__.get(field.attname),
DeferredAttribute): DeferredAttribute):
defers.append(field.attname) defers.append(field.attname)
model = self._meta.proxy_for_model 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): def _get_pk_val(self, meta=None):
if not meta: if not meta:
@ -1008,12 +1010,22 @@ def get_absolute_url(opts, func, self, *args, **kwargs):
class Empty(object): class Empty(object):
pass 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. 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) return cls.__new__(cls)
model_unpickle.__safe_for_unpickle__ = True model_unpickle.__safe_for_unpickle__ = True

View File

@ -36,3 +36,13 @@ class Happening(models.Model):
number2 = models.IntegerField(blank=True, default=Numbers.get_static_number) number2 = models.IntegerField(blank=True, default=Numbers.get_static_number)
number3 = models.IntegerField(blank=True, default=Numbers.get_class_number) number3 = models.IntegerField(blank=True, default=Numbers.get_class_number)
number4 = models.IntegerField(blank=True, default=nn.get_member_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)

View File

@ -3,9 +3,10 @@ from __future__ import absolute_import
import pickle import pickle
import datetime import datetime
from django.db import models
from django.test import TestCase from django.test import TestCase
from .models import Group, Event, Happening from .models import Group, Event, Happening, Container, M2MModel
class PickleabilityTestCase(TestCase): class PickleabilityTestCase(TestCase):
@ -49,3 +50,43 @@ class PickleabilityTestCase(TestCase):
# can't just use assertEqual(original, unpickled) # can't just use assertEqual(original, unpickled)
self.assertEqual(original.__class__, unpickled.__class__) self.assertEqual(original.__class__, unpickled.__class__)
self.assertEqual(original.args, unpickled.args) 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)