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.
This commit is contained in:
Loic Bistuer 2013-07-26 11:59:40 +03:00 committed by Anssi Kääriäinen
parent 8f3aefdec3
commit 31fadc1202
10 changed files with 390 additions and 127 deletions

View File

@ -2,7 +2,7 @@ from functools import wraps
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured 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.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.expressions import F
from django.db.models.manager import Manager from django.db.models.manager import Manager
from django.db.models.base import Model from django.db.models.base import Model

View File

@ -1,4 +1,6 @@
import copy import copy
import inspect
from django.db import router from django.db import router
from django.db.models.query import QuerySet, insert_query, RawQuerySet from django.db.models.query import QuerySet, insert_query, RawQuerySet
from django.db.models import signals 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. # Tracks each time a Manager instance is created. Used to retain order.
creation_counter = 0 creation_counter = 0
def __init__(self): def __init__(self):
super(Manager, self).__init__() super(BaseManager, self).__init__()
self._set_creation_counter() self._set_creation_counter()
self.model = None self.model = None
self._inherited = False self._inherited = False
self._db = None 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): def contribute_to_class(self, model, name):
# TODO: Use weakref because of possible memory leak / circular reference. # TODO: Use weakref because of possible memory leak / circular reference.
self.model = model self.model = model
@ -92,8 +128,8 @@ class Manager(six.with_metaclass(RenameManagerMethods)):
Sets the creation counter value for this instance and increments the Sets the creation counter value for this instance and increments the
class-level copy. class-level copy.
""" """
self.creation_counter = Manager.creation_counter self.creation_counter = BaseManager.creation_counter
Manager.creation_counter += 1 BaseManager.creation_counter += 1
def _copy_to_model(self, model): def _copy_to_model(self, model):
""" """
@ -117,130 +153,30 @@ class Manager(six.with_metaclass(RenameManagerMethods)):
def db(self): def db(self):
return self._db or router.db_for_read(self.model) return self._db or router.db_for_read(self.model)
#######################
# PROXIES TO QUERYSET #
#######################
def get_queryset(self): 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) Returns a new QuerySet object. Subclasses can override this method to
easily customize the behavior of the Manager.
def none(self): """
return self.get_queryset().none() return self._queryset_class(self.model, using=self._db)
def all(self): 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() 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): def _insert(self, objs, fields, **kwargs):
return insert_query(self.model, 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): 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) 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): class ManagerDescriptor(object):
# This class ensures managers aren't accessible via model instances. # This class ensures managers aren't accessible via model instances.

View File

@ -10,7 +10,7 @@ from django.conf import settings
from django.core import exceptions from django.core import exceptions
from django.db import connections, router, transaction, DatabaseError, IntegrityError from django.db import connections, router, transaction, DatabaseError, IntegrityError
from django.db.models.constants import LOOKUP_SEP 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, from django.db.models.query_utils import (Q, select_related_descend,
deferred_class_factory, InvalidQuery) deferred_class_factory, InvalidQuery)
from django.db.models.deletion import Collector from django.db.models.deletion import Collector
@ -30,10 +30,23 @@ REPR_OUTPUT_SIZE = 20
EmptyResultSet = sql.EmptyResultSet 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): class QuerySet(object):
""" """
Represents a lazy database lookup for a set of objects. Represents a lazy database lookup for a set of objects.
""" """
def __init__(self, model=None, query=None, using=None): def __init__(self, model=None, query=None, using=None):
self.model = model self.model = model
self._db = using self._db = using
@ -45,6 +58,13 @@ class QuerySet(object):
self._prefetch_done = False self._prefetch_done = False
self._known_related_objects = {} # {rel_field, {pk: rel_obj}} 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 # # PYTHON MAGIC METHODS #
######################## ########################
@ -70,6 +90,26 @@ class QuerySet(object):
obj_dict = self.__dict__.copy() obj_dict = self.__dict__.copy()
return obj_dict 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): def __repr__(self):
data = list(self[:REPR_OUTPUT_SIZE + 1]) data = list(self[:REPR_OUTPUT_SIZE + 1])
if len(data) > REPR_OUTPUT_SIZE: if len(data) > REPR_OUTPUT_SIZE:
@ -528,6 +568,7 @@ class QuerySet(object):
# Clear the result cache, in case this QuerySet gets reused. # Clear the result cache, in case this QuerySet gets reused.
self._result_cache = None self._result_cache = None
delete.alters_data = True delete.alters_data = True
delete.queryset_only = True
def _raw_delete(self, using): def _raw_delete(self, using):
""" """
@ -567,6 +608,7 @@ class QuerySet(object):
self._result_cache = None self._result_cache = None
return query.get_compiler(self.db).execute_sql(None) return query.get_compiler(self.db).execute_sql(None)
_update.alters_data = True _update.alters_data = True
_update.queryset_only = False
def exists(self): def exists(self):
if self._result_cache is None: if self._result_cache is None:
@ -886,6 +928,15 @@ class QuerySet(object):
def _clone(self, klass=None, setup=False, **kwargs): def _clone(self, klass=None, setup=False, **kwargs):
if klass is None: if klass is None:
klass = self.__class__ 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() query = self.query.clone()
if self._sticky_filter: if self._sticky_filter:
query.filter_is_sticky = True query.filter_is_sticky = True

View File

@ -121,9 +121,7 @@ described here.
QuerySet API QuerySet API
============ ============
Though you usually won't create one manually — you'll go through a Here's the formal declaration of a ``QuerySet``:
:class:`~django.db.models.Manager` — here's the formal declaration of a
``QuerySet``:
.. class:: QuerySet([model=None, query=None, using=None]) .. 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 Note that the queries generated in object deletion is an implementation
detail subject to change. 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:
Field lookups Field lookups

View File

@ -30,6 +30,13 @@ security support until the release of Django 1.8.
What's new in Django 1.7 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 Admin shortcuts support time zones
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -201,6 +201,125 @@ attribute on the manager class. This is documented fully below_.
.. _below: manager-types_ .. _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-inheritance:
Custom managers and model inheritance Custom managers and model inheritance

View File

@ -6,6 +6,7 @@ import threading
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.db import connections, DEFAULT_DB_ALIAS from django.db import connections, DEFAULT_DB_ALIAS
from django.db.models.fields import Field, FieldDoesNotExist 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.db.models.query import QuerySet, EmptyQuerySet, ValuesListQuerySet, MAX_GET_RESULTS
from django.test import TestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature from django.test import TestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature
from django.utils import six from django.utils import six
@ -734,3 +735,57 @@ class ConcurrentSaveTests(TransactionTestCase):
t.join() t.join()
a.save() a.save()
self.assertEqual(Article.objects.get(pk=a.pk).headline, 'foo') 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),
)

View File

@ -20,6 +20,49 @@ class PersonManager(models.Manager):
def get_fun_people(self): def get_fun_people(self):
return self.filter(fun=True) 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 @python_2_unicode_compatible
class Person(models.Model): class Person(models.Model):
first_name = models.CharField(max_length=30) first_name = models.CharField(max_length=30)
@ -27,15 +70,12 @@ class Person(models.Model):
fun = models.BooleanField() fun = models.BooleanField()
objects = PersonManager() objects = PersonManager()
custom_queryset_default_manager = CustomQuerySet.as_manager()
custom_queryset_custom_manager = CustomManager('hello')
def __str__(self): def __str__(self):
return "%s %s" % (self.first_name, self.last_name) 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 @python_2_unicode_compatible
class Book(models.Model): class Book(models.Model):
title = models.CharField(max_length=50) title = models.CharField(max_length=50)

View File

@ -11,12 +11,54 @@ class CustomManagerTests(TestCase):
p1 = Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True) p1 = Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True)
p2 = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False) p2 = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False)
# Test a custom `Manager` method.
self.assertQuerysetEqual( self.assertQuerysetEqual(
Person.objects.get_fun_people(), [ Person.objects.get_fun_people(), [
"Bugs Bunny" "Bugs Bunny"
], ],
six.text_type 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 # The RelatedManager used on the 'books' descriptor extends the default
# manager # manager
self.assertIsInstance(p2.books, PublishedBookManager) self.assertIsInstance(p2.books, PublishedBookManager)

View File

@ -90,3 +90,7 @@ class PickleabilityTestCase(TestCase):
reloaded = pickle.loads(dumped) reloaded = pickle.loads(dumped)
self.assertEqual(original, reloaded) self.assertEqual(original, reloaded)
self.assertIs(reloaded.__class__, dynclass) 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'))