Kaydet (Commit) 5459795e authored tarafından Anssi Kääriäinen's avatar Anssi Kääriäinen

Fixed #20289 -- pickling of dynamic models

üst 855d1305
...@@ -451,16 +451,18 @@ class Model(six.with_metaclass(ModelBase)): ...@@ -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): ...@@ -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
......
...@@ -36,3 +36,13 @@ class Happening(models.Model): ...@@ -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)
...@@ -3,9 +3,10 @@ from __future__ import absolute_import ...@@ -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): ...@@ -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)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment