浏览代码

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 11 年之前
父节点
当前提交
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'))