From ad5afd6ed220ed50a2b48d7ccf9786ac0e52f807 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 27 Mar 2010 15:54:31 +0000 Subject: [PATCH] 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 --- django/db/models/fields/__init__.py | 28 --------------- django/db/models/fields/related.py | 8 ++--- django/utils/functional.py | 9 +++++ django/utils/translation/__init__.py | 34 ++++++++----------- tests/regressiontests/i18n/tests.py | 1 - .../queryset_pickle/__init__.py | 0 .../regressiontests/queryset_pickle/models.py | 8 +++++ .../regressiontests/queryset_pickle/tests.py | 14 ++++++++ 8 files changed, 49 insertions(+), 53 deletions(-) create mode 100644 tests/regressiontests/queryset_pickle/__init__.py create mode 100644 tests/regressiontests/queryset_pickle/models.py create mode 100644 tests/regressiontests/queryset_pickle/tests.py diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 467d13a6f43..281963f3f5c 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -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) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index c6723c68264..5b9a348ca3e 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -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 diff --git a/django/utils/functional.py b/django/utils/functional.py index e52ab76a385..b66fdd3012d 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -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: diff --git a/django/utils/translation/__init__.py b/django/utils/translation/__init__.py index c0a0df9532e..54fb86d4990 100644 --- a/django/utils/translation/__init__.py +++ b/django/utils/translation/__init__.py @@ -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) diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index 31150a69d2c..941f66f3eba 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -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): diff --git a/tests/regressiontests/queryset_pickle/__init__.py b/tests/regressiontests/queryset_pickle/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regressiontests/queryset_pickle/models.py b/tests/regressiontests/queryset_pickle/models.py new file mode 100644 index 00000000000..ec4bbed0967 --- /dev/null +++ b/tests/regressiontests/queryset_pickle/models.py @@ -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) diff --git a/tests/regressiontests/queryset_pickle/tests.py b/tests/regressiontests/queryset_pickle/tests.py new file mode 100644 index 00000000000..8191403be78 --- /dev/null +++ b/tests/regressiontests/queryset_pickle/tests.py @@ -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))