Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #20289 -- pickling of dynamic models

  • Loading branch information...
commit 5459795ef224c5c81461c06a95d38390ee91f014 1 parent 855d130
Anssi Kääriäinen authored May 21, 2013
24  django/db/models/base.py
@@ -451,16 +451,18 @@ def __reduce__(self):
451 451
         need to do things manually, as they're dynamically created classes and
452 452
         only module-level classes can be pickled by the default path.
453 453
         """
454  
-        if not self._deferred:
455  
-            return super(Model, self).__reduce__()
456 454
         data = self.__dict__
  455
+        if not self._deferred:
  456
+            class_id = self._meta.app_label, self._meta.object_name
  457
+            return model_unpickle, (class_id, [], simple_class_factory), data
457 458
         defers = []
458 459
         for field in self._meta.fields:
459 460
             if isinstance(self.__class__.__dict__.get(field.attname),
460  
-                    DeferredAttribute):
  461
+                          DeferredAttribute):
461 462
                 defers.append(field.attname)
462 463
         model = self._meta.proxy_for_model
463  
-        return (model_unpickle, (model, defers), data)
  464
+        class_id = model._meta.app_label, model._meta.object_name
  465
+        return (model_unpickle, (class_id, defers, deferred_class_factory), data)
464 466
 
465 467
     def _get_pk_val(self, meta=None):
466 468
         if not meta:
@@ -1008,12 +1010,22 @@ def get_absolute_url(opts, func, self, *args, **kwargs):
1008 1010
 class Empty(object):
1009 1011
     pass
1010 1012
 
  1013
+def simple_class_factory(model, attrs):
  1014
+    """
  1015
+    Needed for dynamic classes.
  1016
+    """
  1017
+    return model
1011 1018
 
1012  
-def model_unpickle(model, attrs):
  1019
+def model_unpickle(model_id, attrs, factory):
1013 1020
     """
1014 1021
     Used to unpickle Model subclasses with deferred fields.
1015 1022
     """
1016  
-    cls = deferred_class_factory(model, attrs)
  1023
+    if isinstance(model_id, tuple):
  1024
+        model = get_model(*model_id)
  1025
+    else:
  1026
+        # Backwards compat - the model was cached directly in earlier versions.
  1027
+        model = model_id
  1028
+    cls = factory(model, attrs)
1017 1029
     return cls.__new__(cls)
1018 1030
 model_unpickle.__safe_for_unpickle__ = True
1019 1031
 
10  tests/queryset_pickle/models.py
@@ -36,3 +36,13 @@ class Happening(models.Model):
36 36
     number2 = models.IntegerField(blank=True, default=Numbers.get_static_number)
37 37
     number3 = models.IntegerField(blank=True, default=Numbers.get_class_number)
38 38
     number4 = models.IntegerField(blank=True, default=nn.get_member_number)
  39
+
  40
+class Container(object):
  41
+    # To test pickling we need a class that isn't defined on module, but
  42
+    # is still available from app-cache. So, the Container class moves
  43
+    # SomeModel outside of module level
  44
+    class SomeModel(models.Model):
  45
+        somefield = models.IntegerField()
  46
+
  47
+class M2MModel(models.Model):
  48
+    groups = models.ManyToManyField(Group)
43  tests/queryset_pickle/tests.py
@@ -3,9 +3,10 @@
3 3
 import pickle
4 4
 import datetime
5 5
 
  6
+from django.db import models
6 7
 from django.test import TestCase
7 8
 
8  
-from .models import Group, Event, Happening
  9
+from .models import Group, Event, Happening, Container, M2MModel
9 10
 
10 11
 
11 12
 class PickleabilityTestCase(TestCase):
@@ -49,3 +50,43 @@ def test_doesnotexist_exception(self):
49 50
         # can't just use assertEqual(original, unpickled)
50 51
         self.assertEqual(original.__class__, unpickled.__class__)
51 52
         self.assertEqual(original.args, unpickled.args)
  53
+
  54
+    def test_model_pickle(self):
  55
+        """
  56
+        Test that a model not defined on module level is pickleable.
  57
+        """
  58
+        original = Container.SomeModel(pk=1)
  59
+        dumped = pickle.dumps(original)
  60
+        reloaded = pickle.loads(dumped)
  61
+        self.assertEqual(original, reloaded)
  62
+        # Also, deferred dynamic model works
  63
+        Container.SomeModel.objects.create(somefield=1)
  64
+        original = Container.SomeModel.objects.defer('somefield')[0]
  65
+        dumped = pickle.dumps(original)
  66
+        reloaded = pickle.loads(dumped)
  67
+        self.assertEqual(original, reloaded)
  68
+        self.assertEqual(original.somefield, reloaded.somefield)
  69
+
  70
+    def test_model_pickle_m2m(self):
  71
+        """
  72
+        Test intentionally the automatically created through model.
  73
+        """
  74
+        m1 = M2MModel.objects.create()
  75
+        g1 = Group.objects.create(name='foof')
  76
+        m1.groups.add(g1)
  77
+        m2m_through = M2MModel._meta.get_field_by_name('groups')[0].rel.through
  78
+        original = m2m_through.objects.get()
  79
+        dumped = pickle.dumps(original)
  80
+        reloaded = pickle.loads(dumped)
  81
+        self.assertEqual(original, reloaded)
  82
+
  83
+    def test_model_pickle_dynamic(self):
  84
+        class Meta:
  85
+            proxy = True
  86
+        dynclass = type("DynamicEventSubclass", (Event, ),
  87
+                        {'Meta': Meta, '__module__': Event.__module__})
  88
+        original = dynclass(pk=1)
  89
+        dumped = pickle.dumps(original)
  90
+        reloaded = pickle.loads(dumped)
  91
+        self.assertEqual(original, reloaded)
  92
+        self.assertIs(reloaded.__class__, dynclass)

0 notes on commit 5459795

Please sign in to comment.
Something went wrong with that request. Please try again.