From 31fadc120213284da76801cc7bc56e9f32d7281b Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Fri, 26 Jul 2013 11:59:40 +0300 Subject: [PATCH] Fixed #20625 -- Chainable Manager/QuerySet methods. Additionally this patch solves the orthogonal problem that specialized `QuerySet` like `ValuesQuerySet` didn't inherit from the current `QuerySet` type. This wasn't an issue until now because we didn't officially support custom `QuerySet` but it became necessary with the introduction of this new feature. Thanks aaugustin, akaariai, carljm, charettes, mjtamlyn, shaib and timgraham for the reviews. --- django/db/models/__init__.py | 2 +- django/db/models/manager.py | 168 ++++++++++---------------------- django/db/models/query.py | 53 +++++++++- docs/ref/models/querysets.txt | 15 ++- docs/releases/1.7.txt | 7 ++ docs/topics/db/managers.txt | 119 ++++++++++++++++++++++ tests/basic/tests.py | 55 +++++++++++ tests/custom_managers/models.py | 52 ++++++++-- tests/custom_managers/tests.py | 42 ++++++++ tests/queryset_pickle/tests.py | 4 + 10 files changed, 390 insertions(+), 127 deletions(-) diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py index 33151e068dd..2ee525faf1d 100644 --- a/django/db/models/__init__.py +++ b/django/db/models/__init__.py @@ -2,7 +2,7 @@ from functools import wraps from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured from django.db.models.loading import get_apps, get_app_path, get_app_paths, get_app, get_models, get_model, register_models, UnavailableApp -from django.db.models.query import Q +from django.db.models.query import Q, QuerySet from django.db.models.expressions import F from django.db.models.manager import Manager from django.db.models.base import Model diff --git a/django/db/models/manager.py b/django/db/models/manager.py index b369aedb643..f57944ebbc5 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -1,4 +1,6 @@ import copy +import inspect + from django.db import router from django.db.models.query import QuerySet, insert_query, RawQuerySet from django.db.models import signals @@ -56,17 +58,51 @@ class RenameManagerMethods(RenameMethodsBase): ) -class Manager(six.with_metaclass(RenameManagerMethods)): +class BaseManager(six.with_metaclass(RenameManagerMethods)): # Tracks each time a Manager instance is created. Used to retain order. creation_counter = 0 def __init__(self): - super(Manager, self).__init__() + super(BaseManager, self).__init__() self._set_creation_counter() self.model = None self._inherited = False self._db = None + @classmethod + def _get_queryset_methods(cls, queryset_class): + def create_method(name, method): + def manager_method(self, *args, **kwargs): + return getattr(self.get_queryset(), name)(*args, **kwargs) + manager_method.__name__ = method.__name__ + manager_method.__doc__ = method.__doc__ + return manager_method + + new_methods = {} + # Refs http://bugs.python.org/issue1785. + predicate = inspect.isfunction if six.PY3 else inspect.ismethod + for name, method in inspect.getmembers(queryset_class, predicate=predicate): + # Only copy missing methods. + if hasattr(cls, name): + continue + # Only copy public methods or methods with the attribute `queryset_only=False`. + queryset_only = getattr(method, 'queryset_only', None) + if queryset_only or (queryset_only is None and name.startswith('_')): + continue + # Copy the method onto the manager. + new_methods[name] = create_method(name, method) + return new_methods + + @classmethod + def from_queryset(cls, queryset_class, class_name=None): + if class_name is None: + class_name = '%sFrom%s' % (cls.__name__, queryset_class.__name__) + class_dict = { + '_queryset_class': queryset_class, + } + class_dict.update(cls._get_queryset_methods(queryset_class)) + return type(class_name, (cls,), class_dict) + def contribute_to_class(self, model, name): # TODO: Use weakref because of possible memory leak / circular reference. self.model = model @@ -92,8 +128,8 @@ class Manager(six.with_metaclass(RenameManagerMethods)): Sets the creation counter value for this instance and increments the class-level copy. """ - self.creation_counter = Manager.creation_counter - Manager.creation_counter += 1 + self.creation_counter = BaseManager.creation_counter + BaseManager.creation_counter += 1 def _copy_to_model(self, model): """ @@ -117,130 +153,30 @@ class Manager(six.with_metaclass(RenameManagerMethods)): def db(self): return self._db or router.db_for_read(self.model) - ####################### - # PROXIES TO QUERYSET # - ####################### - def get_queryset(self): - """Returns a new QuerySet object. Subclasses can override this method - to easily customize the behavior of the Manager. """ - return QuerySet(self.model, using=self._db) - - def none(self): - return self.get_queryset().none() + Returns a new QuerySet object. Subclasses can override this method to + easily customize the behavior of the Manager. + """ + return self._queryset_class(self.model, using=self._db) def all(self): + # We can't proxy this method through the `QuerySet` like we do for the + # rest of the `QuerySet` methods. This is because `QuerySet.all()` + # works by creating a "copy" of the current queryset and in making said + # copy, all the cached `prefetch_related` lookups are lost. See the + # implementation of `RelatedManager.get_queryset()` for a better + # understanding of how this comes into play. return self.get_queryset() - def count(self): - return self.get_queryset().count() - - def dates(self, *args, **kwargs): - return self.get_queryset().dates(*args, **kwargs) - - def datetimes(self, *args, **kwargs): - return self.get_queryset().datetimes(*args, **kwargs) - - def distinct(self, *args, **kwargs): - return self.get_queryset().distinct(*args, **kwargs) - - def extra(self, *args, **kwargs): - return self.get_queryset().extra(*args, **kwargs) - - def get(self, *args, **kwargs): - return self.get_queryset().get(*args, **kwargs) - - def get_or_create(self, **kwargs): - return self.get_queryset().get_or_create(**kwargs) - - def update_or_create(self, **kwargs): - return self.get_queryset().update_or_create(**kwargs) - - def create(self, **kwargs): - return self.get_queryset().create(**kwargs) - - def bulk_create(self, *args, **kwargs): - return self.get_queryset().bulk_create(*args, **kwargs) - - def filter(self, *args, **kwargs): - return self.get_queryset().filter(*args, **kwargs) - - def aggregate(self, *args, **kwargs): - return self.get_queryset().aggregate(*args, **kwargs) - - def annotate(self, *args, **kwargs): - return self.get_queryset().annotate(*args, **kwargs) - - def complex_filter(self, *args, **kwargs): - return self.get_queryset().complex_filter(*args, **kwargs) - - def exclude(self, *args, **kwargs): - return self.get_queryset().exclude(*args, **kwargs) - - def in_bulk(self, *args, **kwargs): - return self.get_queryset().in_bulk(*args, **kwargs) - - def iterator(self, *args, **kwargs): - return self.get_queryset().iterator(*args, **kwargs) - - def earliest(self, *args, **kwargs): - return self.get_queryset().earliest(*args, **kwargs) - - def latest(self, *args, **kwargs): - return self.get_queryset().latest(*args, **kwargs) - - def first(self): - return self.get_queryset().first() - - def last(self): - return self.get_queryset().last() - - def order_by(self, *args, **kwargs): - return self.get_queryset().order_by(*args, **kwargs) - - def select_for_update(self, *args, **kwargs): - return self.get_queryset().select_for_update(*args, **kwargs) - - def select_related(self, *args, **kwargs): - return self.get_queryset().select_related(*args, **kwargs) - - def prefetch_related(self, *args, **kwargs): - return self.get_queryset().prefetch_related(*args, **kwargs) - - def values(self, *args, **kwargs): - return self.get_queryset().values(*args, **kwargs) - - def values_list(self, *args, **kwargs): - return self.get_queryset().values_list(*args, **kwargs) - - def update(self, *args, **kwargs): - return self.get_queryset().update(*args, **kwargs) - - def reverse(self, *args, **kwargs): - return self.get_queryset().reverse(*args, **kwargs) - - def defer(self, *args, **kwargs): - return self.get_queryset().defer(*args, **kwargs) - - def only(self, *args, **kwargs): - return self.get_queryset().only(*args, **kwargs) - - def using(self, *args, **kwargs): - return self.get_queryset().using(*args, **kwargs) - - def exists(self, *args, **kwargs): - return self.get_queryset().exists(*args, **kwargs) - def _insert(self, objs, fields, **kwargs): return insert_query(self.model, objs, fields, **kwargs) - def _update(self, values, **kwargs): - return self.get_queryset()._update(values, **kwargs) - def raw(self, raw_query, params=None, *args, **kwargs): return RawQuerySet(raw_query=raw_query, model=self.model, params=params, using=self._db, *args, **kwargs) +Manager = BaseManager.from_queryset(QuerySet, class_name='Manager') + class ManagerDescriptor(object): # This class ensures managers aren't accessible via model instances. diff --git a/django/db/models/query.py b/django/db/models/query.py index 087c10de8e2..406838f907f 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -10,7 +10,7 @@ from django.conf import settings from django.core import exceptions from django.db import connections, router, transaction, DatabaseError, IntegrityError from django.db.models.constants import LOOKUP_SEP -from django.db.models.fields import AutoField +from django.db.models.fields import AutoField, Empty from django.db.models.query_utils import (Q, select_related_descend, deferred_class_factory, InvalidQuery) from django.db.models.deletion import Collector @@ -30,10 +30,23 @@ REPR_OUTPUT_SIZE = 20 EmptyResultSet = sql.EmptyResultSet +def _pickle_queryset(class_bases, class_dict): + """ + Used by `__reduce__` to create the initial version of the `QuerySet` class + onto which the output of `__getstate__` will be applied. + + See `__reduce__` for more details. + """ + new = Empty() + new.__class__ = type(class_bases[0].__name__, class_bases, class_dict) + return new + + class QuerySet(object): """ Represents a lazy database lookup for a set of objects. """ + def __init__(self, model=None, query=None, using=None): self.model = model self._db = using @@ -45,6 +58,13 @@ class QuerySet(object): self._prefetch_done = False self._known_related_objects = {} # {rel_field, {pk: rel_obj}} + def as_manager(cls): + # Address the circular dependency between `Queryset` and `Manager`. + from django.db.models.manager import Manager + return Manager.from_queryset(cls)() + as_manager.queryset_only = True + as_manager = classmethod(as_manager) + ######################## # PYTHON MAGIC METHODS # ######################## @@ -70,6 +90,26 @@ class QuerySet(object): obj_dict = self.__dict__.copy() return obj_dict + def __reduce__(self): + """ + Used by pickle to deal with the types that we create dynamically when + specialized queryset such as `ValuesQuerySet` are used in conjunction + with querysets that are *subclasses* of `QuerySet`. + + See `_clone` implementation for more details. + """ + if hasattr(self, '_specialized_queryset_class'): + class_bases = ( + self._specialized_queryset_class, + self._base_queryset_class, + ) + class_dict = { + '_specialized_queryset_class': self._specialized_queryset_class, + '_base_queryset_class': self._base_queryset_class, + } + return _pickle_queryset, (class_bases, class_dict), self.__getstate__() + return super(QuerySet, self).__reduce__() + def __repr__(self): data = list(self[:REPR_OUTPUT_SIZE + 1]) if len(data) > REPR_OUTPUT_SIZE: @@ -528,6 +568,7 @@ class QuerySet(object): # Clear the result cache, in case this QuerySet gets reused. self._result_cache = None delete.alters_data = True + delete.queryset_only = True def _raw_delete(self, using): """ @@ -567,6 +608,7 @@ class QuerySet(object): self._result_cache = None return query.get_compiler(self.db).execute_sql(None) _update.alters_data = True + _update.queryset_only = False def exists(self): if self._result_cache is None: @@ -886,6 +928,15 @@ class QuerySet(object): def _clone(self, klass=None, setup=False, **kwargs): if klass is None: klass = self.__class__ + elif not issubclass(self.__class__, klass): + base_queryset_class = getattr(self, '_base_queryset_class', self.__class__) + class_bases = (klass, base_queryset_class) + class_dict = { + '_base_queryset_class': base_queryset_class, + '_specialized_queryset_class': klass, + } + klass = type(klass.__name__, class_bases, class_dict) + query = self.query.clone() if self._sticky_filter: query.filter_is_sticky = True diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 3963785733b..c3f6a660b47 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -121,9 +121,7 @@ described here. QuerySet API ============ -Though you usually won't create one manually — you'll go through a -:class:`~django.db.models.Manager` — here's the formal declaration of a -``QuerySet``: +Here's the formal declaration of a ``QuerySet``: .. class:: QuerySet([model=None, query=None, using=None]) @@ -1866,6 +1864,17 @@ DO_NOTHING do not prevent taking the fast-path in deletion. Note that the queries generated in object deletion is an implementation detail subject to change. +as_manager +~~~~~~~~~~ + +.. classmethod:: as_manager() + +.. versionadded:: 1.7 + +Class method that returns an instance of :class:`~django.db.models.Manager` +with a copy of the ``QuerySet``'s methods. See +:ref:`create-manager-with-queryset-methods` for more details. + .. _field-lookups: Field lookups diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index bec5aaa12ae..3526b2bce79 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -30,6 +30,13 @@ security support until the release of Django 1.8. What's new in Django 1.7 ======================== +Calling custom ``QuerySet`` methods from the ``Manager`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :meth:`QuerySet.as_manager() ` +class method has been added to :ref:`create Manager with QuerySet methods +`. + Admin shortcuts support time zones ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/db/managers.txt b/docs/topics/db/managers.txt index b940b09d331..3b83865e60e 100644 --- a/docs/topics/db/managers.txt +++ b/docs/topics/db/managers.txt @@ -201,6 +201,125 @@ attribute on the manager class. This is documented fully below_. .. _below: manager-types_ +.. _calling-custom-queryset-methods-from-manager: + +Calling custom ``QuerySet`` methods from the ``Manager`` +-------------------------------------------------------- + +While most methods from the standard ``QuerySet`` are accessible directly from +the ``Manager``, this is only the case for the extra methods defined on a +custom ``QuerySet`` if you also implement them on the ``Manager``:: + + class PersonQuerySet(models.QuerySet): + def male(self): + return self.filter(sex='M') + + def female(self): + return self.filter(sex='F') + + class PersonManager(models.Manager): + def get_queryset(self): + return PersonQuerySet() + + def male(self): + return self.get_queryset().male() + + def female(self): + return self.get_queryset().female() + + class Person(models.Model): + first_name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + sex = models.CharField(max_length=1, choices=(('M', 'Male'), ('F', 'Female'))) + people = PersonManager() + +This example allows you to call both ``male()`` and ``female()`` directly from +the manager ``Person.people``. + +.. _create-manager-with-queryset-methods: + +Creating ``Manager`` with ``QuerySet`` methods +---------------------------------------------- + +.. versionadded:: 1.7 + +In lieu of the above approach which requires duplicating methods on both the +``QuerySet`` and the ``Manager``, :meth:`QuerySet.as_manager() +` can be used to create an instance +of ``Manager`` with a copy of a custom ``QuerySet``'s methods:: + + class Person(models.Model): + ... + people = PersonQuerySet.as_manager() + +The ``Manager`` instance created by :meth:`QuerySet.as_manager() +` will be virtually +identical to the ``PersonManager`` from the previous example. + +Not every ``QuerySet`` method makes sense at the ``Manager`` level; for +instance we intentionally prevent the :meth:`QuerySet.delete() +` method from being copied onto +the ``Manager`` class. + +Methods are copied according to the following rules: + +- Public methods are copied by default. +- Private methods (starting with an underscore) are not copied by default. +- Methods with a `queryset_only` attribute set to `False` are always copied. +- Methods with a `queryset_only` attribute set to `True` are never copied. + +For example:: + + class CustomQuerySet(models.QuerySet): + # Available on both Manager and QuerySet. + def public_method(self): + return + + # Available only on QuerySet. + def _private_method(self): + return + + # Available only on QuerySet. + def opted_out_public_method(self): + return + opted_out_public_method.queryset_only = True + + # Available on both Manager and QuerySet. + def _opted_in_private_method(self): + return + _opted_in_private_method.queryset_only = False + +from_queryset +~~~~~~~~~~~~~ + +.. classmethod:: from_queryset(queryset_class) + +For advance usage you might want both a custom ``Manager`` and a custom +``QuerySet``. You can do that by calling ``Manager.from_queryset()`` which +returns a *subclass* of your base ``Manager`` with a copy of the custom +``QuerySet`` methods:: + + class BaseManager(models.Manager): + def __init__(self, *args, **kwargs): + ... + + def manager_only_method(self): + return + + class CustomQuerySet(models.QuerySet): + def manager_and_queryset_method(self): + return + + class MyModel(models.Model): + objects = BaseManager.from_queryset(CustomQueryset)(*args, **kwargs) + +You may also store the generated class into a variable:: + + CustomManager = BaseManager.from_queryset(CustomQueryset) + + class MyModel(models.Model): + objects = CustomManager(*args, **kwargs) + .. _custom-managers-and-inheritance: Custom managers and model inheritance diff --git a/tests/basic/tests.py b/tests/basic/tests.py index fb21b112796..9d4490afc62 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -6,6 +6,7 @@ import threading from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.db import connections, DEFAULT_DB_ALIAS from django.db.models.fields import Field, FieldDoesNotExist +from django.db.models.manager import BaseManager from django.db.models.query import QuerySet, EmptyQuerySet, ValuesListQuerySet, MAX_GET_RESULTS from django.test import TestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature from django.utils import six @@ -734,3 +735,57 @@ class ConcurrentSaveTests(TransactionTestCase): t.join() a.save() self.assertEqual(Article.objects.get(pk=a.pk).headline, 'foo') + + +class ManagerTest(TestCase): + QUERYSET_PROXY_METHODS = [ + 'none', + 'count', + 'dates', + 'datetimes', + 'distinct', + 'extra', + 'get', + 'get_or_create', + 'update_or_create', + 'create', + 'bulk_create', + 'filter', + 'aggregate', + 'annotate', + 'complex_filter', + 'exclude', + 'in_bulk', + 'iterator', + 'earliest', + 'latest', + 'first', + 'last', + 'order_by', + 'select_for_update', + 'select_related', + 'prefetch_related', + 'values', + 'values_list', + 'update', + 'reverse', + 'defer', + 'only', + 'using', + 'exists', + '_update', + ] + + def test_manager_methods(self): + """ + This test ensures that the correct set of methods from `QuerySet` + are copied onto `Manager`. + + It's particularly useful to prevent accidentally leaking new methods + into `Manager`. New `QuerySet` methods that should also be copied onto + `Manager` will need to be added to `ManagerTest.QUERYSET_PROXY_METHODS`. + """ + self.assertEqual( + sorted(BaseManager._get_queryset_methods(QuerySet).keys()), + sorted(self.QUERYSET_PROXY_METHODS), + ) diff --git a/tests/custom_managers/models.py b/tests/custom_managers/models.py index 2f5e62fc7aa..44d5eb70da9 100644 --- a/tests/custom_managers/models.py +++ b/tests/custom_managers/models.py @@ -20,6 +20,49 @@ class PersonManager(models.Manager): def get_fun_people(self): return self.filter(fun=True) +# An example of a custom manager that sets get_queryset(). + +class PublishedBookManager(models.Manager): + def get_queryset(self): + return super(PublishedBookManager, self).get_queryset().filter(is_published=True) + +# An example of a custom queryset that copies its methods onto the manager. + +class CustomQuerySet(models.QuerySet): + def filter(self, *args, **kwargs): + queryset = super(CustomQuerySet, self).filter(fun=True) + queryset._filter_CustomQuerySet = True + return queryset + + def public_method(self, *args, **kwargs): + return self.all() + + def _private_method(self, *args, **kwargs): + return self.all() + + def optout_public_method(self, *args, **kwargs): + return self.all() + optout_public_method.queryset_only = True + + def _optin_private_method(self, *args, **kwargs): + return self.all() + _optin_private_method.queryset_only = False + +class BaseCustomManager(models.Manager): + def __init__(self, arg): + super(BaseCustomManager, self).__init__() + self.init_arg = arg + + def filter(self, *args, **kwargs): + queryset = super(BaseCustomManager, self).filter(fun=True) + queryset._filter_CustomManager = True + return queryset + + def manager_only(self): + return self.all() + +CustomManager = BaseCustomManager.from_queryset(CustomQuerySet) + @python_2_unicode_compatible class Person(models.Model): first_name = models.CharField(max_length=30) @@ -27,15 +70,12 @@ class Person(models.Model): fun = models.BooleanField() objects = PersonManager() + custom_queryset_default_manager = CustomQuerySet.as_manager() + custom_queryset_custom_manager = CustomManager('hello') + def __str__(self): return "%s %s" % (self.first_name, self.last_name) -# An example of a custom manager that sets get_queryset(). - -class PublishedBookManager(models.Manager): - def get_queryset(self): - return super(PublishedBookManager, self).get_queryset().filter(is_published=True) - @python_2_unicode_compatible class Book(models.Model): title = models.CharField(max_length=50) diff --git a/tests/custom_managers/tests.py b/tests/custom_managers/tests.py index 4fe79fe3fb5..7fa58b2a885 100644 --- a/tests/custom_managers/tests.py +++ b/tests/custom_managers/tests.py @@ -11,12 +11,54 @@ class CustomManagerTests(TestCase): p1 = Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True) p2 = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False) + # Test a custom `Manager` method. self.assertQuerysetEqual( Person.objects.get_fun_people(), [ "Bugs Bunny" ], six.text_type ) + + # Test that the methods of a custom `QuerySet` are properly + # copied onto the default `Manager`. + for manager in ['custom_queryset_default_manager', + 'custom_queryset_custom_manager']: + manager = getattr(Person, manager) + + # Copy public methods. + manager.public_method() + # Don't copy private methods. + with self.assertRaises(AttributeError): + manager._private_method() + # Copy methods with `manager=True` even if they are private. + manager._optin_private_method() + # Don't copy methods with `manager=False` even if they are public. + with self.assertRaises(AttributeError): + manager.optout_public_method() + + # Test that the overriden method is called. + queryset = manager.filter() + self.assertQuerysetEqual(queryset, ["Bugs Bunny"], six.text_type) + self.assertEqual(queryset._filter_CustomQuerySet, True) + + # Test that specialized querysets inherit from our custom queryset. + queryset = manager.values_list('first_name', flat=True).filter() + self.assertEqual(list(queryset), [six.text_type("Bugs")]) + self.assertEqual(queryset._filter_CustomQuerySet, True) + + # Test that the custom manager `__init__()` argument has been set. + self.assertEqual(Person.custom_queryset_custom_manager.init_arg, 'hello') + + # Test that the custom manager method is only available on the manager. + Person.custom_queryset_custom_manager.manager_only() + with self.assertRaises(AttributeError): + Person.custom_queryset_custom_manager.all().manager_only() + + # Test that the queryset method doesn't override the custom manager method. + queryset = Person.custom_queryset_custom_manager.filter() + self.assertQuerysetEqual(queryset, ["Bugs Bunny"], six.text_type) + self.assertEqual(queryset._filter_CustomManager, True) + # The RelatedManager used on the 'books' descriptor extends the default # manager self.assertIsInstance(p2.books, PublishedBookManager) diff --git a/tests/queryset_pickle/tests.py b/tests/queryset_pickle/tests.py index b4b540c80d5..d2f333a9b36 100644 --- a/tests/queryset_pickle/tests.py +++ b/tests/queryset_pickle/tests.py @@ -90,3 +90,7 @@ class PickleabilityTestCase(TestCase): reloaded = pickle.loads(dumped) self.assertEqual(original, reloaded) self.assertIs(reloaded.__class__, dynclass) + + def test_specialized_queryset(self): + self.assert_pickles(Happening.objects.values('name')) + self.assert_pickles(Happening.objects.values('name').dates('when', 'year'))