Ver Fonte

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.
Loic Bistuer há 11 anos atrás
pai
commit
31fadc1202

+ 1 - 1
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

+ 52 - 116
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.

+ 52 - 1
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

+ 12 - 3
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

+ 7 - 0
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() <django.db.models.query.QuerySet.as_manager>`
+class method has been added to :ref:`create Manager with QuerySet methods
+<create-manager-with-queryset-methods>`.
+
 Admin shortcuts support time zones
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 119 - 0
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()
+<django.db.models.query.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()
+<django.db.models.query.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()
+<django.db.models.query.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

+ 55 - 0
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),
+        )

+ 46 - 6
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)

+ 42 - 0
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)

+ 4 - 0
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'))