Fixed #12769, #12924 -- Corrected the pickling of curried and lazy objects, which was preventing queries with translated or related fields from being pickled. And lo, Alex Gaynor didst slayeth the dragon.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@12866 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Russell Keith-Magee 2010-03-27 15:54:31 +00:00
parent b31b2d4da3
commit ad5afd6ed2
8 changed files with 49 additions and 53 deletions

View File

@ -119,34 +119,6 @@ class Field(object):
messages.update(error_messages or {})
self.error_messages = messages
def __getstate__(self):
"""
Pickling support.
"""
from django.utils.functional import Promise
obj_dict = self.__dict__.copy()
items = []
translated_keys = []
for k, v in self.error_messages.items():
if isinstance(v, Promise):
args = getattr(v, '_proxy____args', None)
if args:
translated_keys.append(k)
v = args[0]
items.append((k,v))
obj_dict['_translated_keys'] = translated_keys
obj_dict['error_messages'] = dict(items)
return obj_dict
def __setstate__(self, obj_dict):
"""
Unpickling support.
"""
translated_keys = obj_dict.pop('_translated_keys')
self.__dict__.update(obj_dict)
for k in translated_keys:
self.error_messages[k] = _(self.error_messages[k])
def __cmp__(self, other):
# This is needed because bisect does not take a comparison function.
return cmp(self.creation_counter, other.creation_counter)

View File

@ -88,8 +88,8 @@ class RelatedField(object):
def contribute_to_class(self, cls, name):
sup = super(RelatedField, self)
# Add an accessor to allow easy determination of the related query path for this field
self.related_query_name = curry(self._get_related_query_name, cls._meta)
# Store the opts for related_query_name()
self.opts = cls._meta
if hasattr(sup, 'contribute_to_class'):
sup.contribute_to_class(cls, name)
@ -198,12 +198,12 @@ class RelatedField(object):
v = v[0]
return v
def _get_related_query_name(self, opts):
def related_query_name(self):
# This method defines the name that can be used to identify this
# related object in a table-spanning query. It uses the lower-cased
# object_name by default, but this can be overridden with the
# "related_name" option.
return self.rel.related_name or opts.object_name.lower()
return self.rel.related_name or self.opts.object_name.lower()
class SingleRelatedObjectDescriptor(object):
# This class provides the functionality that makes the related-object

View File

@ -147,6 +147,12 @@ def lazy(func, *resultclasses):
the lazy evaluation code is triggered. Results are not memoized; the
function is evaluated on every access.
"""
# When lazy() is called by the __reduce_ex__ machinery to reconstitute the
# __proxy__ class it can't call with *args, so the first item will just be
# a tuple.
if len(resultclasses) == 1 and isinstance(resultclasses[0], tuple):
resultclasses = resultclasses[0]
class __proxy__(Promise):
"""
Encapsulate a function call and act as a proxy for methods that are
@ -162,6 +168,9 @@ def lazy(func, *resultclasses):
if self.__dispatch is None:
self.__prepare_class__()
def __reduce_ex__(self, protocol):
return (lazy, (self.__func, resultclasses), self.__dict__)
def __prepare_class__(cls):
cls.__dispatch = {}
for resultclass in resultclasses:

View File

@ -1,8 +1,11 @@
"""
Internationalization support.
"""
from django.utils.functional import lazy
from django.conf import settings
from django.utils.encoding import force_unicode
from django.utils.functional import lazy, curry
from django.utils.translation import trans_real, trans_null
__all__ = ['gettext', 'gettext_noop', 'gettext_lazy', 'ngettext',
'ngettext_lazy', 'string_concat', 'activate', 'deactivate',
@ -19,32 +22,23 @@ __all__ = ['gettext', 'gettext_noop', 'gettext_lazy', 'ngettext',
# replace the functions with their real counterparts (once we do access the
# settings).
def delayed_loader(*args, **kwargs):
def delayed_loader(real_name, *args, **kwargs):
"""
Replace each real_* function with the corresponding function from either
trans_real or trans_null (e.g. real_gettext is replaced with
trans_real.gettext or trans_null.gettext). This function is run once, the
first time any i18n method is called. It replaces all the i18n methods at
once at that time.
Call the real, underlying function. We have a level of indirection here so
that modules can use the translation bits without actually requiring
Django's settings bits to be configured before import.
"""
import traceback
from django.conf import settings
if settings.USE_I18N:
import trans_real as trans
trans = trans_real
else:
import trans_null as trans
caller = traceback.extract_stack(limit=2)[0][2]
g = globals()
for name in __all__:
if hasattr(trans, name):
g['real_%s' % name] = getattr(trans, name)
trans = trans_null
# Make the originally requested function call on the way out the door.
return g['real_%s' % caller](*args, **kwargs)
return getattr(trans, real_name)(*args, **kwargs)
g = globals()
for name in __all__:
g['real_%s' % name] = delayed_loader
g['real_%s' % name] = curry(delayed_loader, name)
del g, delayed_loader
def gettext_noop(message):
@ -102,10 +96,10 @@ def templatize(src):
def deactivate_all():
return real_deactivate_all()
def string_concat(*strings):
def _string_concat(*strings):
"""
Lazy variant of string concatenation, needed for translations that are
constructed from multiple parts.
"""
return u''.join([force_unicode(s) for s in strings])
string_concat = lazy(string_concat, unicode)
string_concat = lazy(_string_concat, unicode)

View File

@ -46,7 +46,6 @@ class TranslationTests(TestCase):
unicode(string_concat(...)) should not raise a TypeError - #4796
"""
import django.utils.translation
self.assertEqual(django.utils.translation, reload(django.utils.translation))
self.assertEqual(u'django', unicode(django.utils.translation.string_concat("dja", "ngo")))
def test_safe_status(self):

View File

@ -0,0 +1,8 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
class Group(models.Model):
name = models.CharField(_('name'), max_length=100)
class Event(models.Model):
group = models.ForeignKey(Group)

View File

@ -0,0 +1,14 @@
import pickle
from django.test import TestCase
from models import Group, Event
class PickleabilityTestCase(TestCase):
def assert_pickles(self, qs):
self.assertEqual(list(pickle.loads(pickle.dumps(qs))), list(qs))
def test_related_field(self):
g = Group.objects.create(name="Ponies Who Own Maybachs")
self.assert_pickles(Event.objects.filter(group=g.id))