mirror of
https://github.com/django/django.git
synced 2024-12-23 01:25:58 +00:00
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:
parent
b31b2d4da3
commit
ad5afd6ed2
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
0
tests/regressiontests/queryset_pickle/__init__.py
Normal file
0
tests/regressiontests/queryset_pickle/__init__.py
Normal file
8
tests/regressiontests/queryset_pickle/models.py
Normal file
8
tests/regressiontests/queryset_pickle/models.py
Normal 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)
|
14
tests/regressiontests/queryset_pickle/tests.py
Normal file
14
tests/regressiontests/queryset_pickle/tests.py
Normal 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))
|
Loading…
Reference in New Issue
Block a user