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

Fixed #19635 -- Made fields pickleable

parent 3beabb5a
Loading
Loading
Loading
Loading
+40 −0
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ from base64 import b64decode, b64encode
from itertools import tee

from django.db import connection
from django.db.models.loading import get_model
from django.db.models.query_utils import QueryWrapper
from django.conf import settings
from django import forms
@@ -24,6 +25,9 @@ from django.utils.encoding import smart_text, force_text, force_bytes
from django.utils.ipv6 import clean_ipv6_address
from django.utils import six

class Empty(object):
    pass

class NOT_PROVIDED:
    pass

@@ -31,6 +35,9 @@ class NOT_PROVIDED:
# of most "choices" lists.
BLANK_CHOICE_DASH = [("", "---------")]

def _load_field(app_label, model_name, field_name):
    return get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0]

class FieldDoesNotExist(Exception):
    pass

@@ -49,6 +56,11 @@ class FieldDoesNotExist(Exception):
#
#     getattr(obj, opts.pk.attname)

def _empty(of_cls):
    new = Empty()
    new.__class__ = of_cls
    return new

@total_ordering
class Field(object):
    """Base class for all field types"""
@@ -148,6 +160,34 @@ class Field(object):
        memodict[id(self)] = obj
        return obj

    def __copy__(self):
        # We need to avoid hitting __reduce__, so define this
        # slightly weird copy construct.
        obj = Empty()
        obj.__class__ = self.__class__
        obj.__dict__ = self.__dict__.copy()
        return obj

    def __reduce__(self):
        """
        Pickling should return the model._meta.fields instance of the field,
        not a new copy of that field. So, we use the app cache to load the
        model and then the field back.
        """
        if not hasattr(self, 'model'):
            # Fields are sometimes used without attaching them to models (for
            # example in aggregation). In this case give back a plain field
            # instance. The code below will create a new empty instance of
            # class self.__class__, then update its dict with self.__dict__
            # values - so, this is very close to normal pickle.
            return _empty, (self.__class__,), self.__dict__
        if self.model._deferred:
            # Deferred model will not be found from the app cache. This could
            # be fixed by reconstructing the deferred model on unpickle.
            raise RuntimeError("Fields of deferred models can't be reduced")
        return _load_field, (self.model._meta.app_label, self.model._meta.object_name,
                             self.name)

    def to_python(self, value):
        """
        Converts the input value into the expected Python data type, raising
+0 −48
Original line number Diff line number Diff line
@@ -189,54 +189,6 @@ class Query(object):
        memo[id(self)] = result
        return result

    def __getstate__(self):
        """
        Pickling support.
        """
        obj_dict = self.__dict__.copy()
        obj_dict['related_select_cols'] = []

        # Fields can't be pickled, so if a field list has been
        # specified, we pickle the list of field names instead.
        # None is also a possible value; that can pass as-is
        obj_dict['select'] = [
            (s.col, s.field is not None and s.field.name or None)
            for s in obj_dict['select']
        ]
        # alias_map can also contain references to fields.
        new_alias_map = {}
        for alias, join_info in obj_dict['alias_map'].items():
            if join_info.join_field is None:
                new_alias_map[alias] = join_info
            else:
                model = join_info.join_field.model._meta
                field_id = (model.app_label, model.object_name, join_info.join_field.name)
                new_alias_map[alias] = join_info._replace(join_field=field_id)
        obj_dict['alias_map'] = new_alias_map
        return obj_dict

    def __setstate__(self, obj_dict):
        """
        Unpickling support.
        """
        # Rebuild list of field instances
        opts = obj_dict['model']._meta
        obj_dict['select'] = [
            SelectInfo(tpl[0], tpl[1] is not None and opts.get_field(tpl[1]) or None)
            for tpl in obj_dict['select']
        ]
        new_alias_map = {}
        for alias, join_info in obj_dict['alias_map'].items():
            if join_info.join_field is None:
                new_alias_map[alias] = join_info
            else:
                field_id = join_info.join_field
                new_alias_map[alias] = join_info._replace(
                    join_field=get_model(field_id[0], field_id[1])._meta.get_field(field_id[2]))
        obj_dict['alias_map'] = new_alias_map

        self.__dict__.update(obj_dict)

    def prepare(self):
        return self

+0 −24
Original line number Diff line number Diff line
@@ -345,30 +345,6 @@ class Constraint(object):
    def __init__(self, alias, col, field):
        self.alias, self.col, self.field = alias, col, field

    def __getstate__(self):
        """Save the state of the Constraint for pickling.

        Fields aren't necessarily pickleable, because they can have
        callable default values. So, instead of pickling the field
        store a reference so we can restore it manually
        """
        obj_dict = self.__dict__.copy()
        if self.field:
            obj_dict['model'] = self.field.model
            obj_dict['field_name'] = self.field.name
        del obj_dict['field']
        return obj_dict

    def __setstate__(self, data):
        """Restore the constraint """
        model = data.pop('model', None)
        field_name = data.pop('field_name', None)
        self.__dict__.update(data)
        if model is not None:
            self.field = model._meta.get_field(field_name)
        else:
            self.field = None

    def prepare(self, lookup_type, value):
        if self.field:
            return self.field.get_prep_lookup(lookup_type, value)