Skip to content

Commit

Permalink
Fixed #19635 -- Made fields pickleable
Browse files Browse the repository at this point in the history
  • Loading branch information
akaariai committed Mar 17, 2013
1 parent 3beabb5 commit f403653
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 72 deletions.
40 changes: 40 additions & 0 deletions django/db/models/fields/__init__.py
Expand Up @@ -10,6 +10,7 @@
from itertools import tee from itertools import tee


from django.db import connection from django.db import connection
from django.db.models.loading import get_model
from django.db.models.query_utils import QueryWrapper from django.db.models.query_utils import QueryWrapper
from django.conf import settings from django.conf import settings
from django import forms from django import forms
Expand All @@ -24,13 +25,19 @@
from django.utils.ipv6 import clean_ipv6_address from django.utils.ipv6 import clean_ipv6_address
from django.utils import six from django.utils import six


class Empty(object):
pass

class NOT_PROVIDED: class NOT_PROVIDED:
pass pass


# The values to use for "blank" in SelectFields. Will be appended to the start # The values to use for "blank" in SelectFields. Will be appended to the start
# of most "choices" lists. # of most "choices" lists.
BLANK_CHOICE_DASH = [("", "---------")] 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): class FieldDoesNotExist(Exception):
pass pass


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


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

@total_ordering @total_ordering
class Field(object): class Field(object):
"""Base class for all field types""" """Base class for all field types"""
Expand Down Expand Up @@ -148,6 +160,34 @@ def __deepcopy__(self, memodict):
memodict[id(self)] = obj memodict[id(self)] = obj
return 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): def to_python(self, value):
""" """
Converts the input value into the expected Python data type, raising Converts the input value into the expected Python data type, raising
Expand Down
48 changes: 0 additions & 48 deletions django/db/models/sql/query.py
Expand Up @@ -189,54 +189,6 @@ def __deepcopy__(self, memo):
memo[id(self)] = result memo[id(self)] = result
return 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): def prepare(self):
return self return self


Expand Down
24 changes: 0 additions & 24 deletions django/db/models/sql/where.py
Expand Up @@ -345,30 +345,6 @@ class Constraint(object):
def __init__(self, alias, col, field): def __init__(self, alias, col, field):
self.alias, self.col, self.field = 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): def prepare(self, lookup_type, value):
if self.field: if self.field:
return self.field.get_prep_lookup(lookup_type, value) return self.field.get_prep_lookup(lookup_type, value)
Expand Down

0 comments on commit f403653

Please sign in to comment.