Fixed #20932, #25897 -- Streamlined manager inheritance.

This commit is contained in:
Loïc Bistuer 2016-02-19 02:27:55 +07:00
parent 9935f97cd2
commit 3a47d42fa3
12 changed files with 112 additions and 218 deletions

View File

@ -500,8 +500,7 @@ class ModelState(object):
else: else:
# Force this manager to be the first and thus default # Force this manager to be the first and thus default
managers_mapping[default_manager_name] = (0, models.Manager()) managers_mapping[default_manager_name] = (0, models.Manager())
# Sort all managers by their creation counter for manager in model._meta.managers:
for _, manager, _ in sorted(model._meta.managers):
if manager.name == "_base_manager" or not manager.use_in_migrations: if manager.name == "_base_manager" or not manager.use_in_migrations:
continue continue
reconstruct_manager(manager) reconstruct_manager(manager)

View File

@ -151,18 +151,6 @@ class ModelBase(type):
if is_proxy and base_meta and base_meta.swapped: if is_proxy and base_meta and base_meta.swapped:
raise TypeError("%s cannot proxy the swapped model '%s'." % (name, base_meta.swapped)) raise TypeError("%s cannot proxy the swapped model '%s'." % (name, base_meta.swapped))
if getattr(new_class, '_default_manager', None):
if not is_proxy:
# Multi-table inheritance doesn't inherit default manager from
# parents.
new_class._default_manager = None
new_class._base_manager = None
else:
# Proxy classes do inherit parent's default manager, if none is
# set explicitly.
new_class._default_manager = new_class._default_manager._copy_to_model(new_class)
new_class._base_manager = new_class._base_manager._copy_to_model(new_class)
# Add all attributes to the class. # Add all attributes to the class.
for obj_name, obj in attrs.items(): for obj_name, obj in attrs.items():
new_class.add_to_class(obj_name, obj) new_class.add_to_class(obj_name, obj)
@ -217,7 +205,6 @@ class ModelBase(type):
inherited_attributes = set() inherited_attributes = set()
# Do the appropriate setup for any model parents. # Do the appropriate setup for any model parents.
for base in new_class.mro(): for base in new_class.mro():
original_base = base
if base not in parents or not hasattr(base, '_meta'): if base not in parents or not hasattr(base, '_meta'):
# Things without _meta aren't functional models, so they're # Things without _meta aren't functional models, so they're
# uninteresting parents. # uninteresting parents.
@ -294,14 +281,6 @@ class ModelBase(type):
# Pass any non-abstract parent classes onto child. # Pass any non-abstract parent classes onto child.
new_class._meta.parents.update(base_parents) new_class._meta.parents.update(base_parents)
# Inherit managers from the abstract base classes.
new_class.copy_managers(base._meta.abstract_managers)
# Proxy models inherit the non-abstract managers from their base,
# unless they have redefined any of them.
if is_proxy:
new_class.copy_managers(original_base._meta.concrete_managers)
# Inherit private fields (like GenericForeignKey) from the parent # Inherit private fields (like GenericForeignKey) from the parent
# class # class
for field in base._meta.private_fields: for field in base._meta.private_fields:
@ -330,15 +309,6 @@ class ModelBase(type):
new_class._meta.apps.register_model(new_class._meta.app_label, new_class) new_class._meta.apps.register_model(new_class._meta.app_label, new_class)
return new_class return new_class
def copy_managers(cls, base_managers):
# This is in-place sorting of an Options attribute, but that's fine.
base_managers.sort()
for _, mgr_name, manager in base_managers: # NOQA (redefinition of _)
val = getattr(cls, mgr_name, None)
if not val or val is manager:
new_manager = manager._copy_to_model(cls)
cls.add_to_class(mgr_name, new_manager)
def add_to_class(cls, name, value): def add_to_class(cls, name, value):
# We should call the contribute_to_class method only if it's bound # We should call the contribute_to_class method only if it's bound
if not inspect.isclass(value) and hasattr(value, 'contribute_to_class'): if not inspect.isclass(value) and hasattr(value, 'contribute_to_class'):
@ -376,6 +346,7 @@ class ModelBase(type):
setattr(cls, 'get_absolute_url', get_absolute_url_override) setattr(cls, 'get_absolute_url', get_absolute_url_override)
ensure_default_manager(cls) ensure_default_manager(cls)
signals.class_prepared.send(sender=cls) signals.class_prepared.send(sender=cls)
@ -1263,7 +1234,7 @@ class Model(six.with_metaclass(ModelBase)):
""" Perform all manager checks. """ """ Perform all manager checks. """
errors = [] errors = []
for __, manager, __ in cls._meta.managers: for manager in cls._meta.managers:
errors.extend(manager.check(**kwargs)) errors.extend(manager.check(**kwargs))
return errors return errors

View File

@ -8,43 +8,40 @@ from django.utils import six
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
def ensure_default_manager(cls): def can_use_for_related_field(manager_class):
return manager_class is Manager or getattr(manager_class, 'use_for_related_fields', False)
def ensure_default_manager(model):
""" """
Ensures that a Model subclass contains a default manager and sets the Ensures that a Model subclass contains a default manager and sets the
_default_manager attribute on the class. Also sets up the _base_manager _default_manager and _base_manager attributes on the class.
points to a plain Manager instance (which could be the same as
_default_manager if it's not a subclass of Manager).
""" """
if cls._meta.swapped:
setattr(cls, 'objects', SwappedManagerDescriptor(cls)) if not model._meta.managers:
return if any(f.name == 'objects' for f in model._meta.fields):
if not getattr(cls, '_default_manager', None):
if any(f.name == 'objects' for f in cls._meta.fields):
raise ValueError( raise ValueError(
"Model %s must specify a custom Manager, because it has a " "Model %s must specify a custom Manager, because it has a "
"field named 'objects'" % cls.__name__ "field named 'objects'" % model.__name__
) )
# Create the default manager, if needed. model.add_to_class('objects', Manager())
cls.add_to_class('objects', Manager())
cls._base_manager = cls.objects model._default_manager = model._meta.managers[0]
elif not getattr(cls, '_base_manager', None):
default_mgr = cls._default_manager.__class__ # Just alias _base_manager if default manager is suitable.
if (default_mgr is Manager or if can_use_for_related_field(model._default_manager.__class__):
getattr(default_mgr, "use_for_related_fields", False)): model._base_manager = model._default_manager
cls._base_manager = cls._default_manager
# Otherwise search for a suitable manager type in the default manager MRO.
else:
for base_manager_class in model._default_manager.__class__.mro()[1:]:
if can_use_for_related_field(base_manager_class):
model._base_manager = base_manager_class()
model._base_manager.name = '_base_manager'
model._base_manager.model = model
break
else: else:
# Default manager isn't a plain Manager class, or a suitable raise ValueError("Could not find a suitable base manager.")
# replacement, so we walk up the base class hierarchy until we hit
# something appropriate.
for base_class in default_mgr.mro()[1:]:
if (base_class is Manager or
getattr(base_class, "use_for_related_fields", False)):
cls.add_to_class('_base_manager', base_class())
return
raise AssertionError(
"Should never get here. Please report a bug, including your "
"model and model manager setup."
)
@python_2_unicode_compatible @python_2_unicode_compatible
@ -67,7 +64,6 @@ class BaseManager(object):
self._set_creation_counter() self._set_creation_counter()
self.model = None self.model = None
self.name = None self.name = None
self._inherited = False
self._db = None self._db = None
self._hints = {} self._hints = {}
@ -150,26 +146,13 @@ class BaseManager(object):
return type(class_name, (cls,), class_dict) 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.
self.model = model
if not self.name: if not self.name:
self.name = name self.name = name
# Only contribute the manager if the model is concrete self.model = model
if model._meta.abstract:
setattr(model, name, AbstractManagerDescriptor(model))
elif model._meta.swapped:
setattr(model, name, SwappedManagerDescriptor(model))
else:
# if not model._meta.abstract and not model._meta.swapped:
setattr(model, name, ManagerDescriptor(self))
if (not getattr(model, '_default_manager', None) or
self.creation_counter < model._default_manager.creation_counter):
model._default_manager = self
abstract = False setattr(model, name, ManagerDescriptor(self))
if model._meta.abstract or (self._inherited and not self.model._meta.proxy):
abstract = True model._meta.add_manager(self)
model._meta.managers.append((self.creation_counter, self, abstract))
def _set_creation_counter(self): def _set_creation_counter(self):
""" """
@ -179,19 +162,6 @@ class BaseManager(object):
self.creation_counter = BaseManager.creation_counter self.creation_counter = BaseManager.creation_counter
BaseManager.creation_counter += 1 BaseManager.creation_counter += 1
def _copy_to_model(self, model):
"""
Makes a copy of the manager and assigns it to 'model', which should be
a child of the existing model (used when inheriting a manager from an
abstract base class).
"""
assert issubclass(model, self.model)
mgr = copy.copy(self)
mgr._set_creation_counter()
mgr.model = model
mgr._inherited = True
return mgr
def db_manager(self, using=None, hints=None): def db_manager(self, using=None, hints=None):
obj = copy.copy(self) obj = copy.copy(self)
obj._db = using or self._db obj._db = using or self._db
@ -240,43 +210,29 @@ class Manager(BaseManager.from_queryset(QuerySet)):
class ManagerDescriptor(object): class ManagerDescriptor(object):
# This class ensures managers aren't accessible via model instances.
# For example, Poll.objects works, but poll_obj.objects raises AttributeError.
def __init__(self, manager): def __init__(self, manager):
self.manager = manager self.manager = manager
def __get__(self, instance, cls=None): def __get__(self, instance, cls=None):
if instance is not None: if instance is not None:
raise AttributeError("Manager isn't accessible via %s instances" % cls.__name__) raise AttributeError("Manager isn't accessible via %s instances" % cls.__name__)
return self.manager
if cls._meta.abstract:
raise AttributeError("Manager isn't available; %s is abstract" % (
cls._meta.object_name,
))
class AbstractManagerDescriptor(object): if cls._meta.swapped:
# This class provides a better error message when you try to access a raise AttributeError(
# manager on an abstract model. "Manager isn't available; '%s.%s' has been swapped for '%s'" % (
def __init__(self, model): cls._meta.app_label,
self.model = model cls._meta.object_name,
cls._meta.swapped,
def __get__(self, instance, cls=None): )
raise AttributeError("Manager isn't available; %s is abstract" % (
self.model._meta.object_name,
))
class SwappedManagerDescriptor(object):
# This class provides a better error message when you try to access a
# manager on a swapped model.
def __init__(self, model):
self.model = model
def __get__(self, instance, cls=None):
raise AttributeError(
"Manager isn't available; '%s.%s' has been swapped for '%s'" % (
self.model._meta.app_label,
self.model._meta.object_name,
self.model._meta.swapped,
) )
)
return cls._meta.managers_map[self.manager.name]
class EmptyManager(Manager): class EmptyManager(Manager):

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import copy
import warnings import warnings
from bisect import bisect from bisect import bisect
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
@ -73,7 +74,8 @@ def make_immutable_fields_list(name, data):
@python_2_unicode_compatible @python_2_unicode_compatible
class Options(object): class Options(object):
FORWARD_PROPERTIES = {'fields', 'many_to_many', 'concrete_fields', FORWARD_PROPERTIES = {'fields', 'many_to_many', 'concrete_fields',
'local_concrete_fields', '_forward_fields_map'} 'local_concrete_fields', '_forward_fields_map',
'managers', 'managers_map'}
REVERSE_PROPERTIES = {'related_objects', 'fields_map', '_relation_tree'} REVERSE_PROPERTIES = {'related_objects', 'fields_map', '_relation_tree'}
default_apps = apps default_apps = apps
@ -83,6 +85,7 @@ class Options(object):
self.local_fields = [] self.local_fields = []
self.local_many_to_many = [] self.local_many_to_many = []
self.private_fields = [] self.private_fields = []
self.local_managers = []
self.model_name = None self.model_name = None
self.verbose_name = None self.verbose_name = None
self.verbose_name_plural = None self.verbose_name_plural = None
@ -122,12 +125,6 @@ class Options(object):
self.parents = OrderedDict() self.parents = OrderedDict()
self.auto_created = False self.auto_created = False
# To handle various inheritance situations, we need to track where
# managers came from (concrete or abstract base classes). `managers`
# keeps a list of 3-tuples of the form:
# (creation_counter, instance, abstract(=True))
self.managers = []
# List of all lookups defined in ForeignKey 'limit_choices_to' options # List of all lookups defined in ForeignKey 'limit_choices_to' options
# from *other* models. Needed for some admin checks. Internal use only. # from *other* models. Needed for some admin checks. Internal use only.
self.related_fkey_lookups = [] self.related_fkey_lookups = []
@ -154,20 +151,6 @@ class Options(object):
def installed(self): def installed(self):
return self.app_config is not None return self.app_config is not None
@property
def abstract_managers(self):
return [
(counter, instance.name, instance) for counter, instance, abstract
in self.managers if abstract
]
@property
def concrete_managers(self):
return [
(counter, instance.name, instance) for counter, instance, abstract
in self.managers if not abstract
]
def contribute_to_class(self, cls, name): def contribute_to_class(self, cls, name):
from django.db import connection from django.db import connection
from django.db.backends.utils import truncate_name from django.db.backends.utils import truncate_name
@ -264,6 +247,10 @@ class Options(object):
auto = AutoField(verbose_name='ID', primary_key=True, auto_created=True) auto = AutoField(verbose_name='ID', primary_key=True, auto_created=True)
model.add_to_class('id', auto) model.add_to_class('id', auto)
def add_manager(self, manager):
self.local_managers.append(manager)
self._expire_cache()
def add_field(self, field, private=False, virtual=NOT_PROVIDED): def add_field(self, field, private=False, virtual=NOT_PROVIDED):
if virtual is not NOT_PROVIDED: if virtual is not NOT_PROVIDED:
warnings.warn( warnings.warn(
@ -371,6 +358,25 @@ class Options(object):
return swapped_for return swapped_for
return None return None
@cached_property
def managers(self):
managers = []
bases = (b for b in self.model.mro() if hasattr(b, '_meta'))
for depth, base in enumerate(bases):
for manager in base._meta.local_managers:
manager = copy.copy(manager)
manager.model = self.model
managers.append((depth, manager.creation_counter, manager))
return make_immutable_fields_list(
"managers",
(m[2] for m in sorted(managers)),
)
@cached_property
def managers_map(self):
return {manager.name: manager for manager in reversed(self.managers)}
@cached_property @cached_property
def fields(self): def fields(self):
""" """

View File

@ -321,33 +321,26 @@ You may also store the generated class into a variable::
Custom managers and model inheritance Custom managers and model inheritance
------------------------------------- -------------------------------------
Class inheritance and model managers aren't quite a perfect match for each Here's how Django handles custom managers and
other. Managers are often specific to the classes they are defined on and
inheriting them in subclasses isn't necessarily a good idea. Also, because the
first manager declared is the *default manager*, it is important to allow that
to be controlled. So here's how Django handles custom managers and
:ref:`model inheritance <model-inheritance>`: :ref:`model inheritance <model-inheritance>`:
1. Managers defined on non-abstract base classes are *not* inherited by 1. Managers from base classes are always inherited by the child class,
child classes. If you want to reuse a manager from a non-abstract base, using Python's normal name resolution order (names on the child
redeclare it explicitly on the child class. These sorts of managers are
likely to be fairly specific to the class they are defined on, so
inheriting them can often lead to unexpected results (particularly as
far as the default manager goes). Therefore, they aren't passed onto
child classes.
2. Managers from abstract base classes are always inherited by the child
class, using Python's normal name resolution order (names on the child
class override all others; then come names on the first parent class, class override all others; then come names on the first parent class,
and so on). Abstract base classes are designed to capture information and so on).
and behavior that is common to their child classes. Defining common
managers is an appropriate part of this common information.
3. The default manager on a class is either the first manager declared on 2. The default manager on a class is either the first manager declared on the
the class, if that exists, or the default manager of the first abstract class, if that exists, or the default manager of the first parent class in
base class in the parent hierarchy, if that exists. If no default the parent hierarchy, if that exists. If no manager is explicitly declared,
manager is explicitly declared, Django's normal default manager is Django automatically creates the `objects` manager and it becomes the default
used. manager.
.. versionchanged:: 1.10
In older versions, manager inheritance varied depending on the type of
model inheritance (i.e. :ref:`abstract-base-classes`,
:ref:`multi-table-inheritance`, or :ref:`proxy-models`), especially
with regards to electing the default manager.
These rules provide the necessary flexibility if you want to install a These rules provide the necessary flexibility if you want to install a
collection of custom managers on a group of models, via an abstract base collection of custom managers on a group of models, via an abstract base

View File

@ -1287,33 +1287,19 @@ Differences between proxy inheritance and unmanaged models
Proxy model inheritance might look fairly similar to creating an unmanaged Proxy model inheritance might look fairly similar to creating an unmanaged
model, using the :attr:`~django.db.models.Options.managed` attribute on a model, using the :attr:`~django.db.models.Options.managed` attribute on a
model's ``Meta`` class. The two alternatives are not quite the same and it's model's ``Meta`` class.
worth considering which one you should use.
One difference is that you can (and, in fact, must unless you want an empty With careful setting of :attr:`Meta.db_table
model) specify model fields on models with ``Meta.managed=False``. You could, <django.db.models.Options.db_table>` you could create an unmanaged model that
with careful setting of :attr:`Meta.db_table shadows an existing model and adds Python methods to it. However, that would be
<django.db.models.Options.db_table>` create an unmanaged model that shadowed very repetitive and fragile as you need to keep both copies synchronized if you
an existing model and add Python methods to it. However, that would be very
repetitive and fragile as you need to keep both copies synchronized if you
make any changes. make any changes.
The other difference that is more important for proxy models, is how model On the other hand, proxy models are intended to behave exactly like the model
managers are handled. Proxy models are intended to behave exactly like the they are proxying for. They are always in sync with the parent model since they
model they are proxying for. So they inherit the parent model's managers, directly inherit its fields and managers.
including the default manager. In the normal multi-table model inheritance
case, children do not inherit managers from their parents as the custom
managers aren't always appropriate when extra fields are involved. The
:ref:`manager documentation <custom-managers-and-inheritance>` has more
details about this latter case.
When these two features were implemented, attempts were made to squash them The general rules are:
into a single option. It turned out that interactions with inheritance, in
general, and managers, in particular, made the API very complicated and
potentially difficult to understand and use. It turned out that two options
were needed in any case, so the current separation arose.
So, the general rules are:
1. If you are mirroring an existing model or database table and don't want 1. If you are mirroring an existing model or database table and don't want
all the original database table columns, use ``Meta.managed=False``. all the original database table columns, use ``Meta.managed=False``.

View File

@ -3,29 +3,16 @@ from __future__ import unicode_literals
import warnings import warnings
from django.apps import apps
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, User from django.contrib.auth.models import AnonymousUser, User
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db import IntegrityError from django.db import IntegrityError
from django.dispatch import receiver
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.test.signals import setting_changed
from django.utils import translation from django.utils import translation
from .models import CustomUser from .models import CustomUser
@receiver(setting_changed)
def user_model_swapped(**kwargs):
if kwargs['setting'] == 'AUTH_USER_MODEL':
from django.db.models.manager import ensure_default_manager
# Reset User manager
setattr(User, 'objects', User._default_manager)
ensure_default_manager(User)
apps.clear_cache()
class BasicTestCase(TestCase): class BasicTestCase(TestCase):
def test_user(self): def test_user(self):
"Check that users can be created and can set their password" "Check that users can be created and can set their password"

View File

@ -115,14 +115,12 @@ class Child5(AbstractBase3):
return self.name return self.name
# Will inherit managers from AbstractBase1, but not Child4.
class Child6(Child4): class Child6(Child4):
value = models.IntegerField() value = models.IntegerField()
# Will not inherit default manager from parent.
class Child7(Parent): class Child7(Parent):
pass objects = models.Manager()
# RelatedManagers # RelatedManagers

View File

@ -50,7 +50,7 @@ class ManagersRegressionTests(TestCase):
]) ])
self.assertQuerysetEqual(Child4.manager1.all(), ["<Child4: d1>", "<Child4: f1>"], ordered=False) self.assertQuerysetEqual(Child4.manager1.all(), ["<Child4: d1>", "<Child4: f1>"], ordered=False)
self.assertQuerysetEqual(Child5._default_manager.all(), ["<Child5: fred>"]) self.assertQuerysetEqual(Child5._default_manager.all(), ["<Child5: fred>"])
self.assertQuerysetEqual(Child6._default_manager.all(), ["<Child6: f1>"]) self.assertQuerysetEqual(Child6._default_manager.all(), ["<Child6: f1>", "<Child6: f2>"], ordered=False)
self.assertQuerysetEqual( self.assertQuerysetEqual(
Child7._default_manager.order_by('name'), Child7._default_manager.order_by('name'),
["<Child7: barney>", "<Child7: fred>"] ["<Child7: barney>", "<Child7: fred>"]

View File

@ -600,13 +600,13 @@ class ManyToOneTests(TestCase):
# If the manager is marked "use_for_related_fields", it'll get used instead # If the manager is marked "use_for_related_fields", it'll get used instead
# of the "bare" queryset. Usually you'd define this as a property on the class, # of the "bare" queryset. Usually you'd define this as a property on the class,
# but this approximates that in a way that's easier in tests. # but this approximates that in a way that's easier in tests.
School.objects.use_for_related_fields = True School._default_manager.use_for_related_fields = True
try: try:
private_student = Student.objects.get(pk=private_student.pk) private_student = Student.objects.get(pk=private_student.pk)
with self.assertRaises(School.DoesNotExist): with self.assertRaises(School.DoesNotExist):
private_student.school private_student.school
finally: finally:
School.objects.use_for_related_fields = False School._default_manager.use_for_related_fields = False
def test_hasattr_related_object(self): def test_hasattr_related_object(self):
# The exception raised on attribute access when a related object # The exception raised on attribute access when a related object

View File

@ -262,13 +262,11 @@ class StateTests(SimpleTestCase):
self.assertEqual(len(new_apps.get_model("migrations", "SubTag")._meta.local_fields), 2) self.assertEqual(len(new_apps.get_model("migrations", "SubTag")._meta.local_fields), 2)
Food = new_apps.get_model("migrations", "Food") Food = new_apps.get_model("migrations", "Food")
managers = sorted(Food._meta.managers) self.assertEqual([mgr.name for mgr in Food._meta.managers],
self.assertEqual([mgr.name for _, mgr, _ in managers],
['default', 'food_mgr1', 'food_mgr2']) ['default', 'food_mgr1', 'food_mgr2'])
self.assertTrue(all(isinstance(mgr.name, six.text_type) for _, mgr, _ in managers)) self.assertTrue(all(isinstance(mgr.name, six.text_type) for mgr in Food._meta.managers))
self.assertEqual([mgr.__class__ for _, mgr, _ in managers], self.assertEqual([mgr.__class__ for mgr in Food._meta.managers],
[models.Manager, FoodManager, FoodManager]) [models.Manager, FoodManager, FoodManager])
self.assertIs(managers[0][1], Food._default_manager)
def test_render_model_inheritance(self): def test_render_model_inheritance(self):
class Book(models.Model): class Book(models.Model):

View File

@ -457,21 +457,21 @@ class OneToOneTests(TestCase):
# If the manager is marked "use_for_related_fields", it'll get used instead # If the manager is marked "use_for_related_fields", it'll get used instead
# of the "bare" queryset. Usually you'd define this as a property on the class, # of the "bare" queryset. Usually you'd define this as a property on the class,
# but this approximates that in a way that's easier in tests. # but this approximates that in a way that's easier in tests.
School.objects.use_for_related_fields = True School._default_manager.use_for_related_fields = True
try: try:
private_director = Director._base_manager.get(pk=private_director.pk) private_director = Director._base_manager.get(pk=private_director.pk)
with self.assertRaises(School.DoesNotExist): with self.assertRaises(School.DoesNotExist):
private_director.school private_director.school
finally: finally:
School.objects.use_for_related_fields = False School._default_manager.use_for_related_fields = False
Director.objects.use_for_related_fields = True Director._default_manager.use_for_related_fields = True
try: try:
private_school = School._base_manager.get(pk=private_school.pk) private_school = School._base_manager.get(pk=private_school.pk)
with self.assertRaises(Director.DoesNotExist): with self.assertRaises(Director.DoesNotExist):
private_school.director private_school.director
finally: finally:
Director.objects.use_for_related_fields = False Director._default_manager.use_for_related_fields = False
def test_hasattr_related_object(self): def test_hasattr_related_object(self):
# The exception raised on attribute access when a related object # The exception raised on attribute access when a related object