Fixed #26207 -- Replaced dynamic classes with non-data descriptors for deferred instance loading.
This commit is contained in:
parent
dac075e910
commit
7f51876f99
|
@ -165,8 +165,7 @@ class AppConfig(object):
|
||||||
raise LookupError(
|
raise LookupError(
|
||||||
"App '%s' doesn't have a '%s' model." % (self.label, model_name))
|
"App '%s' doesn't have a '%s' model." % (self.label, model_name))
|
||||||
|
|
||||||
def get_models(self, include_auto_created=False,
|
def get_models(self, include_auto_created=False, include_swapped=False):
|
||||||
include_deferred=False, include_swapped=False):
|
|
||||||
"""
|
"""
|
||||||
Returns an iterable of models.
|
Returns an iterable of models.
|
||||||
|
|
||||||
|
@ -182,8 +181,6 @@ class AppConfig(object):
|
||||||
"""
|
"""
|
||||||
self.check_models_ready()
|
self.check_models_ready()
|
||||||
for model in self.models.values():
|
for model in self.models.values():
|
||||||
if model._deferred and not include_deferred:
|
|
||||||
continue
|
|
||||||
if model._meta.auto_created and not include_auto_created:
|
if model._meta.auto_created and not include_auto_created:
|
||||||
continue
|
continue
|
||||||
if model._meta.swapped and not include_swapped:
|
if model._meta.swapped and not include_swapped:
|
||||||
|
|
|
@ -156,8 +156,7 @@ class Apps(object):
|
||||||
|
|
||||||
# This method is performance-critical at least for Django's test suite.
|
# This method is performance-critical at least for Django's test suite.
|
||||||
@lru_cache.lru_cache(maxsize=None)
|
@lru_cache.lru_cache(maxsize=None)
|
||||||
def get_models(self, include_auto_created=False,
|
def get_models(self, include_auto_created=False, include_swapped=False):
|
||||||
include_deferred=False, include_swapped=False):
|
|
||||||
"""
|
"""
|
||||||
Returns a list of all installed models.
|
Returns a list of all installed models.
|
||||||
|
|
||||||
|
@ -174,8 +173,7 @@ class Apps(object):
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for app_config in self.app_configs.values():
|
for app_config in self.app_configs.values():
|
||||||
result.extend(list(app_config.get_models(
|
result.extend(list(app_config.get_models(include_auto_created, include_swapped)))
|
||||||
include_auto_created, include_deferred, include_swapped)))
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_model(self, app_label, model_name=None):
|
def get_model(self, app_label, model_name=None):
|
||||||
|
|
|
@ -27,8 +27,6 @@ class ContentTypeManager(models.Manager):
|
||||||
def _get_opts(self, model, for_concrete_model):
|
def _get_opts(self, model, for_concrete_model):
|
||||||
if for_concrete_model:
|
if for_concrete_model:
|
||||||
model = model._meta.concrete_model
|
model = model._meta.concrete_model
|
||||||
elif model._deferred:
|
|
||||||
model = model._meta.proxy_for_model
|
|
||||||
return model._meta
|
return model._meta
|
||||||
|
|
||||||
def _get_from_cache(self, opts):
|
def _get_from_cache(self, opts):
|
||||||
|
|
|
@ -5,10 +5,11 @@ objects corresponding to geographic model fields.
|
||||||
|
|
||||||
Thanks to Robert Coup for providing this functionality (see #4322).
|
Thanks to Robert Coup for providing this functionality (see #4322).
|
||||||
"""
|
"""
|
||||||
|
from django.db.models.query_utils import DeferredAttribute
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
|
||||||
|
|
||||||
class SpatialProxy(object):
|
class SpatialProxy(DeferredAttribute):
|
||||||
def __init__(self, klass, field):
|
def __init__(self, klass, field):
|
||||||
"""
|
"""
|
||||||
Proxy initializes on the given Geometry or Raster class (not an instance)
|
Proxy initializes on the given Geometry or Raster class (not an instance)
|
||||||
|
@ -16,6 +17,7 @@ class SpatialProxy(object):
|
||||||
"""
|
"""
|
||||||
self._field = field
|
self._field = field
|
||||||
self._klass = klass
|
self._klass = klass
|
||||||
|
super(SpatialProxy, self).__init__(field.attname, klass)
|
||||||
|
|
||||||
def __get__(self, instance, cls=None):
|
def __get__(self, instance, cls=None):
|
||||||
"""
|
"""
|
||||||
|
@ -29,7 +31,10 @@ class SpatialProxy(object):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
# Getting the value of the field.
|
# Getting the value of the field.
|
||||||
geo_value = instance.__dict__[self._field.attname]
|
try:
|
||||||
|
geo_value = instance.__dict__[self._field.attname]
|
||||||
|
except KeyError:
|
||||||
|
geo_value = super(SpatialProxy, self).__get__(instance, cls)
|
||||||
|
|
||||||
if isinstance(geo_value, self._klass):
|
if isinstance(geo_value, self._klass):
|
||||||
geo_obj = geo_value
|
geo_obj = geo_value
|
||||||
|
|
|
@ -37,8 +37,7 @@ class Serializer(base.Serializer):
|
||||||
self._current = None
|
self._current = None
|
||||||
|
|
||||||
def get_dump_object(self, obj):
|
def get_dump_object(self, obj):
|
||||||
model = obj._meta.proxy_for_model if obj._deferred else obj.__class__
|
data = OrderedDict([('model', force_text(obj._meta))])
|
||||||
data = OrderedDict([('model', force_text(model._meta))])
|
|
||||||
if not self.use_natural_primary_keys or not hasattr(obj, 'natural_key'):
|
if not self.use_natural_primary_keys or not hasattr(obj, 'natural_key'):
|
||||||
data["pk"] = force_text(obj._get_pk_val(), strings_only=True)
|
data["pk"] = force_text(obj._get_pk_val(), strings_only=True)
|
||||||
data['fields'] = self._current
|
data['fields'] = self._current
|
||||||
|
|
|
@ -52,8 +52,7 @@ class Serializer(base.Serializer):
|
||||||
raise base.SerializationError("Non-model object (%s) encountered during serialization" % type(obj))
|
raise base.SerializationError("Non-model object (%s) encountered during serialization" % type(obj))
|
||||||
|
|
||||||
self.indent(1)
|
self.indent(1)
|
||||||
model = obj._meta.proxy_for_model if obj._deferred else obj.__class__
|
attrs = OrderedDict([("model", smart_text(obj._meta))])
|
||||||
attrs = OrderedDict([("model", smart_text(model._meta))])
|
|
||||||
if not self.use_natural_primary_keys or not hasattr(obj, 'natural_key'):
|
if not self.use_natural_primary_keys or not hasattr(obj, 'natural_key'):
|
||||||
obj_pk = obj._get_pk_val()
|
obj_pk = obj._get_pk_val()
|
||||||
if obj_pk is not None:
|
if obj_pk is not None:
|
||||||
|
|
|
@ -19,7 +19,7 @@ from django.db.models.query import ( # NOQA
|
||||||
)
|
)
|
||||||
|
|
||||||
# Imports that would create circular imports if sorted
|
# Imports that would create circular imports if sorted
|
||||||
from django.db.models.base import Model # NOQA isort:skip
|
from django.db.models.base import DEFERRED, Model # NOQA isort:skip
|
||||||
from django.db.models.fields.related import ( # NOQA isort:skip
|
from django.db.models.fields.related import ( # NOQA isort:skip
|
||||||
ForeignKey, ForeignObject, OneToOneField, ManyToManyField,
|
ForeignKey, ForeignObject, OneToOneField, ManyToManyField,
|
||||||
ManyToOneRel, ManyToManyRel, OneToOneRel,
|
ManyToOneRel, ManyToManyRel, OneToOneRel,
|
||||||
|
|
|
@ -27,12 +27,11 @@ from django.db.models.fields.related import (
|
||||||
from django.db.models.manager import ensure_default_manager
|
from django.db.models.manager import ensure_default_manager
|
||||||
from django.db.models.options import Options
|
from django.db.models.options import Options
|
||||||
from django.db.models.query import Q
|
from django.db.models.query import Q
|
||||||
from django.db.models.query_utils import (
|
|
||||||
DeferredAttribute, deferred_class_factory,
|
|
||||||
)
|
|
||||||
from django.db.models.utils import make_model_tuple
|
from django.db.models.utils import make_model_tuple
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.encoding import force_str, force_text
|
from django.utils.encoding import (
|
||||||
|
force_str, force_text, python_2_unicode_compatible,
|
||||||
|
)
|
||||||
from django.utils.functional import curry
|
from django.utils.functional import curry
|
||||||
from django.utils.six.moves import zip
|
from django.utils.six.moves import zip
|
||||||
from django.utils.text import capfirst, get_text_list
|
from django.utils.text import capfirst, get_text_list
|
||||||
|
@ -40,6 +39,17 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.version import get_version
|
from django.utils.version import get_version
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class Deferred(object):
|
||||||
|
def __repr__(self):
|
||||||
|
return str('<Deferred field>')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str('<Deferred field>')
|
||||||
|
|
||||||
|
DEFERRED = Deferred()
|
||||||
|
|
||||||
|
|
||||||
def subclass_exception(name, parents, module, attached_to=None):
|
def subclass_exception(name, parents, module, attached_to=None):
|
||||||
"""
|
"""
|
||||||
Create exception subclass. Used by ModelBase below.
|
Create exception subclass. Used by ModelBase below.
|
||||||
|
@ -353,7 +363,6 @@ class ModelState(object):
|
||||||
|
|
||||||
|
|
||||||
class Model(six.with_metaclass(ModelBase)):
|
class Model(six.with_metaclass(ModelBase)):
|
||||||
_deferred = False
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
signals.pre_init.send(sender=self.__class__, args=args, kwargs=kwargs)
|
signals.pre_init.send(sender=self.__class__, args=args, kwargs=kwargs)
|
||||||
|
@ -377,11 +386,15 @@ class Model(six.with_metaclass(ModelBase)):
|
||||||
# is *not* consumed. We rely on this, so don't change the order
|
# is *not* consumed. We rely on this, so don't change the order
|
||||||
# without changing the logic.
|
# without changing the logic.
|
||||||
for val, field in zip(args, fields_iter):
|
for val, field in zip(args, fields_iter):
|
||||||
|
if val is DEFERRED:
|
||||||
|
continue
|
||||||
setattr(self, field.attname, val)
|
setattr(self, field.attname, val)
|
||||||
else:
|
else:
|
||||||
# Slower, kwargs-ready version.
|
# Slower, kwargs-ready version.
|
||||||
fields_iter = iter(self._meta.fields)
|
fields_iter = iter(self._meta.fields)
|
||||||
for val, field in zip(args, fields_iter):
|
for val, field in zip(args, fields_iter):
|
||||||
|
if val is DEFERRED:
|
||||||
|
continue
|
||||||
setattr(self, field.attname, val)
|
setattr(self, field.attname, val)
|
||||||
kwargs.pop(field.name, None)
|
kwargs.pop(field.name, None)
|
||||||
# Maintain compatibility with existing calls.
|
# Maintain compatibility with existing calls.
|
||||||
|
@ -393,13 +406,8 @@ class Model(six.with_metaclass(ModelBase)):
|
||||||
|
|
||||||
for field in fields_iter:
|
for field in fields_iter:
|
||||||
is_related_object = False
|
is_related_object = False
|
||||||
# This slightly odd construct is so that we can access any
|
# Virtual field
|
||||||
# data-descriptor object (DeferredAttribute) without triggering its
|
if field.attname not in kwargs and field.column is None:
|
||||||
# __get__ method.
|
|
||||||
if (field.attname not in kwargs and
|
|
||||||
(isinstance(self.__class__.__dict__.get(field.attname), DeferredAttribute) or
|
|
||||||
field.column is None)):
|
|
||||||
# This field will be populated on request.
|
|
||||||
continue
|
continue
|
||||||
if kwargs:
|
if kwargs:
|
||||||
if isinstance(field.remote_field, ForeignObjectRel):
|
if isinstance(field.remote_field, ForeignObjectRel):
|
||||||
|
@ -435,15 +443,18 @@ class Model(six.with_metaclass(ModelBase)):
|
||||||
# field.name instead of field.attname (e.g. "user" instead of
|
# field.name instead of field.attname (e.g. "user" instead of
|
||||||
# "user_id") so that the object gets properly cached (and type
|
# "user_id") so that the object gets properly cached (and type
|
||||||
# checked) by the RelatedObjectDescriptor.
|
# checked) by the RelatedObjectDescriptor.
|
||||||
setattr(self, field.name, rel_obj)
|
if rel_obj is not DEFERRED:
|
||||||
|
setattr(self, field.name, rel_obj)
|
||||||
else:
|
else:
|
||||||
setattr(self, field.attname, val)
|
if val is not DEFERRED:
|
||||||
|
setattr(self, field.attname, val)
|
||||||
|
|
||||||
if kwargs:
|
if kwargs:
|
||||||
for prop in list(kwargs):
|
for prop in list(kwargs):
|
||||||
try:
|
try:
|
||||||
if isinstance(getattr(self.__class__, prop), property):
|
if isinstance(getattr(self.__class__, prop), property):
|
||||||
setattr(self, prop, kwargs[prop])
|
if kwargs[prop] is not DEFERRED:
|
||||||
|
setattr(self, prop, kwargs[prop])
|
||||||
del kwargs[prop]
|
del kwargs[prop]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
@ -454,10 +465,11 @@ class Model(six.with_metaclass(ModelBase)):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_db(cls, db, field_names, values):
|
def from_db(cls, db, field_names, values):
|
||||||
if cls._deferred:
|
if len(values) != len(cls._meta.concrete_fields):
|
||||||
new = cls(**dict(zip(field_names, values)))
|
values = list(values)
|
||||||
else:
|
values.reverse()
|
||||||
new = cls(*values)
|
values = [values.pop() if f.attname in field_names else DEFERRED for f in cls._meta.concrete_fields]
|
||||||
|
new = cls(*values)
|
||||||
new._state.adding = False
|
new._state.adding = False
|
||||||
new._state.db = db
|
new._state.db = db
|
||||||
return new
|
return new
|
||||||
|
@ -501,17 +513,8 @@ class Model(six.with_metaclass(ModelBase)):
|
||||||
"""
|
"""
|
||||||
data = self.__dict__
|
data = self.__dict__
|
||||||
data[DJANGO_VERSION_PICKLE_KEY] = get_version()
|
data[DJANGO_VERSION_PICKLE_KEY] = get_version()
|
||||||
if not self._deferred:
|
class_id = self._meta.app_label, self._meta.object_name
|
||||||
class_id = self._meta.app_label, self._meta.object_name
|
return model_unpickle, (class_id,), data
|
||||||
return model_unpickle, (class_id, [], simple_class_factory), data
|
|
||||||
defers = []
|
|
||||||
for field in self._meta.fields:
|
|
||||||
if isinstance(self.__class__.__dict__.get(field.attname),
|
|
||||||
DeferredAttribute):
|
|
||||||
defers.append(field.attname)
|
|
||||||
model = self._meta.proxy_for_model
|
|
||||||
class_id = model._meta.app_label, model._meta.object_name
|
|
||||||
return (model_unpickle, (class_id, defers, deferred_class_factory), data)
|
|
||||||
|
|
||||||
def __setstate__(self, state):
|
def __setstate__(self, state):
|
||||||
msg = None
|
msg = None
|
||||||
|
@ -547,7 +550,7 @@ class Model(six.with_metaclass(ModelBase)):
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
f.attname for f in self._meta.concrete_fields
|
f.attname for f in self._meta.concrete_fields
|
||||||
if isinstance(self.__class__.__dict__.get(f.attname), DeferredAttribute)
|
if f.attname not in self.__dict__
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh_from_db(self, using=None, fields=None, **kwargs):
|
def refresh_from_db(self, using=None, fields=None, **kwargs):
|
||||||
|
@ -574,18 +577,14 @@ class Model(six.with_metaclass(ModelBase)):
|
||||||
'are not allowed in fields.' % LOOKUP_SEP)
|
'are not allowed in fields.' % LOOKUP_SEP)
|
||||||
|
|
||||||
db = using if using is not None else self._state.db
|
db = using if using is not None else self._state.db
|
||||||
if self._deferred:
|
db_instance_qs = self.__class__._default_manager.using(db).filter(pk=self.pk)
|
||||||
non_deferred_model = self._meta.proxy_for_model
|
|
||||||
else:
|
|
||||||
non_deferred_model = self.__class__
|
|
||||||
db_instance_qs = non_deferred_model._default_manager.using(db).filter(pk=self.pk)
|
|
||||||
|
|
||||||
# Use provided fields, if not set then reload all non-deferred fields.
|
# Use provided fields, if not set then reload all non-deferred fields.
|
||||||
|
deferred_fields = self.get_deferred_fields()
|
||||||
if fields is not None:
|
if fields is not None:
|
||||||
fields = list(fields)
|
fields = list(fields)
|
||||||
db_instance_qs = db_instance_qs.only(*fields)
|
db_instance_qs = db_instance_qs.only(*fields)
|
||||||
elif self._deferred:
|
elif deferred_fields:
|
||||||
deferred_fields = self.get_deferred_fields()
|
|
||||||
fields = [f.attname for f in self._meta.concrete_fields
|
fields = [f.attname for f in self._meta.concrete_fields
|
||||||
if f.attname not in deferred_fields]
|
if f.attname not in deferred_fields]
|
||||||
db_instance_qs = db_instance_qs.only(*fields)
|
db_instance_qs = db_instance_qs.only(*fields)
|
||||||
|
@ -664,6 +663,7 @@ class Model(six.with_metaclass(ModelBase)):
|
||||||
if force_insert and (force_update or update_fields):
|
if force_insert and (force_update or update_fields):
|
||||||
raise ValueError("Cannot force both insert and updating in model saving.")
|
raise ValueError("Cannot force both insert and updating in model saving.")
|
||||||
|
|
||||||
|
deferred_fields = self.get_deferred_fields()
|
||||||
if update_fields is not None:
|
if update_fields is not None:
|
||||||
# If update_fields is empty, skip the save. We do also check for
|
# If update_fields is empty, skip the save. We do also check for
|
||||||
# no-op saves later on for inheritance cases. This bailout is
|
# no-op saves later on for inheritance cases. This bailout is
|
||||||
|
@ -690,17 +690,11 @@ class Model(six.with_metaclass(ModelBase)):
|
||||||
|
|
||||||
# If saving to the same database, and this model is deferred, then
|
# If saving to the same database, and this model is deferred, then
|
||||||
# automatically do a "update_fields" save on the loaded fields.
|
# automatically do a "update_fields" save on the loaded fields.
|
||||||
elif not force_insert and self._deferred and using == self._state.db:
|
elif not force_insert and deferred_fields and using == self._state.db:
|
||||||
field_names = set()
|
field_names = set()
|
||||||
for field in self._meta.concrete_fields:
|
for field in self._meta.concrete_fields:
|
||||||
if not field.primary_key and not hasattr(field, 'through'):
|
if not field.primary_key and not hasattr(field, 'through'):
|
||||||
field_names.add(field.attname)
|
field_names.add(field.attname)
|
||||||
deferred_fields = [
|
|
||||||
f.attname for f in self._meta.fields
|
|
||||||
if (f.attname not in self.__dict__ and
|
|
||||||
isinstance(self.__class__.__dict__[f.attname], DeferredAttribute))
|
|
||||||
]
|
|
||||||
|
|
||||||
loaded_fields = field_names.difference(deferred_fields)
|
loaded_fields = field_names.difference(deferred_fields)
|
||||||
if loaded_fields:
|
if loaded_fields:
|
||||||
update_fields = frozenset(loaded_fields)
|
update_fields = frozenset(loaded_fields)
|
||||||
|
@ -1662,14 +1656,7 @@ def make_foreign_order_accessors(model, related_model):
|
||||||
########
|
########
|
||||||
|
|
||||||
|
|
||||||
def simple_class_factory(model, attrs):
|
def model_unpickle(model_id):
|
||||||
"""
|
|
||||||
Needed for dynamic classes.
|
|
||||||
"""
|
|
||||||
return model
|
|
||||||
|
|
||||||
|
|
||||||
def model_unpickle(model_id, attrs, factory):
|
|
||||||
"""
|
"""
|
||||||
Used to unpickle Model subclasses with deferred fields.
|
Used to unpickle Model subclasses with deferred fields.
|
||||||
"""
|
"""
|
||||||
|
@ -1678,8 +1665,7 @@ def model_unpickle(model_id, attrs, factory):
|
||||||
else:
|
else:
|
||||||
# Backwards compat - the model was cached directly in earlier versions.
|
# Backwards compat - the model was cached directly in earlier versions.
|
||||||
model = model_id
|
model = model_id
|
||||||
cls = factory(model, attrs)
|
return model.__new__(model)
|
||||||
return cls.__new__(cls)
|
|
||||||
model_unpickle.__safe_for_unpickle__ = True
|
model_unpickle.__safe_for_unpickle__ = True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ from django.core import checks, exceptions, validators
|
||||||
# purposes.
|
# purposes.
|
||||||
from django.core.exceptions import FieldDoesNotExist # NOQA
|
from django.core.exceptions import FieldDoesNotExist # NOQA
|
||||||
from django.db import connection, connections, router
|
from django.db import connection, connections, router
|
||||||
from django.db.models.query_utils import RegisterLookupMixin
|
from django.db.models.query_utils import DeferredAttribute, RegisterLookupMixin
|
||||||
from django.utils import six, timezone
|
from django.utils import six, timezone
|
||||||
from django.utils.datastructures import DictWrapper
|
from django.utils.datastructures import DictWrapper
|
||||||
from django.utils.dateparse import (
|
from django.utils.dateparse import (
|
||||||
|
@ -504,10 +504,6 @@ class Field(RegisterLookupMixin):
|
||||||
# class self.__class__, then update its dict with self.__dict__
|
# class self.__class__, then update its dict with self.__dict__
|
||||||
# values - so, this is very close to normal pickle.
|
# values - so, this is very close to normal pickle.
|
||||||
return _empty, (self.__class__,), self.__dict__
|
return _empty, (self.__class__,), self.__dict__
|
||||||
if self.model._deferred:
|
|
||||||
# Deferred model will not be found from the app registry. This
|
|
||||||
# could be fixed by reconstructing the deferred model on unpickle.
|
|
||||||
raise RuntimeError("Fields of deferred models can't be reduced")
|
|
||||||
return _load_field, (self.model._meta.app_label, self.model._meta.object_name,
|
return _load_field, (self.model._meta.app_label, self.model._meta.object_name,
|
||||||
self.name)
|
self.name)
|
||||||
|
|
||||||
|
@ -696,6 +692,12 @@ class Field(RegisterLookupMixin):
|
||||||
cls._meta.add_field(self, private=True)
|
cls._meta.add_field(self, private=True)
|
||||||
else:
|
else:
|
||||||
cls._meta.add_field(self)
|
cls._meta.add_field(self)
|
||||||
|
if self.column:
|
||||||
|
# Don't override classmethods with the descriptor. This means that
|
||||||
|
# if you have a classmethod and a field with the same name, then
|
||||||
|
# such fields can't be deferred (we don't have a check for this).
|
||||||
|
if not getattr(cls, self.attname, None):
|
||||||
|
setattr(cls, self.attname, DeferredAttribute(self.attname, cls))
|
||||||
if self.choices:
|
if self.choices:
|
||||||
setattr(cls, 'get_%s_display' % self.name,
|
setattr(cls, 'get_%s_display' % self.name,
|
||||||
curry(cls._get_FIELD_display, field=self))
|
curry(cls._get_FIELD_display, field=self))
|
||||||
|
|
|
@ -19,7 +19,7 @@ from django.db.models.deletion import Collector
|
||||||
from django.db.models.expressions import Date, DateTime, F
|
from django.db.models.expressions import Date, DateTime, F
|
||||||
from django.db.models.fields import AutoField
|
from django.db.models.fields import AutoField
|
||||||
from django.db.models.query_utils import (
|
from django.db.models.query_utils import (
|
||||||
InvalidQuery, Q, check_rel_lookup_compatibility, deferred_class_factory,
|
InvalidQuery, Q, check_rel_lookup_compatibility,
|
||||||
)
|
)
|
||||||
from django.db.models.sql.constants import CURSOR
|
from django.db.models.sql.constants import CURSOR
|
||||||
from django.utils import six, timezone
|
from django.utils import six, timezone
|
||||||
|
@ -60,11 +60,6 @@ class ModelIterable(BaseIterable):
|
||||||
model_fields_start, model_fields_end = select_fields[0], select_fields[-1] + 1
|
model_fields_start, model_fields_end = select_fields[0], select_fields[-1] + 1
|
||||||
init_list = [f[0].target.attname
|
init_list = [f[0].target.attname
|
||||||
for f in select[model_fields_start:model_fields_end]]
|
for f in select[model_fields_start:model_fields_end]]
|
||||||
if len(init_list) != len(model_cls._meta.concrete_fields):
|
|
||||||
init_set = set(init_list)
|
|
||||||
skip = [f.attname for f in model_cls._meta.concrete_fields
|
|
||||||
if f.attname not in init_set]
|
|
||||||
model_cls = deferred_class_factory(model_cls, skip)
|
|
||||||
related_populators = get_related_populators(klass_info, select, db)
|
related_populators = get_related_populators(klass_info, select, db)
|
||||||
for row in compiler.results_iter(results):
|
for row in compiler.results_iter(results):
|
||||||
obj = model_cls.from_db(db, init_list, row[model_fields_start:model_fields_end])
|
obj = model_cls.from_db(db, init_list, row[model_fields_start:model_fields_end])
|
||||||
|
@ -1254,9 +1249,7 @@ class RawQuerySet(object):
|
||||||
if skip:
|
if skip:
|
||||||
if self.model._meta.pk.attname in skip:
|
if self.model._meta.pk.attname in skip:
|
||||||
raise InvalidQuery('Raw query must include the primary key')
|
raise InvalidQuery('Raw query must include the primary key')
|
||||||
model_cls = deferred_class_factory(self.model, skip)
|
model_cls = self.model
|
||||||
else:
|
|
||||||
model_cls = self.model
|
|
||||||
fields = [self.model_fields.get(c) for c in self.columns]
|
fields = [self.model_fields.get(c) for c in self.columns]
|
||||||
converters = compiler.get_converters([
|
converters = compiler.get_converters([
|
||||||
f.get_col(f.model._meta.db_table) if f else None for f in fields
|
f.get_col(f.model._meta.db_table) if f else None for f in fields
|
||||||
|
@ -1718,7 +1711,7 @@ class RelatedPopulator(object):
|
||||||
return [row[row_pos] for row_pos in pos_list]
|
return [row[row_pos] for row_pos in pos_list]
|
||||||
self.reorder_for_init = reorder_for_init
|
self.reorder_for_init = reorder_for_init
|
||||||
|
|
||||||
self.model_cls = self.get_deferred_cls(klass_info, self.init_list)
|
self.model_cls = klass_info['model']
|
||||||
self.pk_idx = self.init_list.index(self.model_cls._meta.pk.attname)
|
self.pk_idx = self.init_list.index(self.model_cls._meta.pk.attname)
|
||||||
self.related_populators = get_related_populators(klass_info, select, self.db)
|
self.related_populators = get_related_populators(klass_info, select, self.db)
|
||||||
field = klass_info['field']
|
field = klass_info['field']
|
||||||
|
@ -1732,17 +1725,6 @@ class RelatedPopulator(object):
|
||||||
if field.unique:
|
if field.unique:
|
||||||
self.reverse_cache_name = field.remote_field.get_cache_name()
|
self.reverse_cache_name = field.remote_field.get_cache_name()
|
||||||
|
|
||||||
def get_deferred_cls(self, klass_info, init_list):
|
|
||||||
model_cls = klass_info['model']
|
|
||||||
if len(init_list) != len(model_cls._meta.concrete_fields):
|
|
||||||
init_set = set(init_list)
|
|
||||||
skip = [
|
|
||||||
f.attname for f in model_cls._meta.concrete_fields
|
|
||||||
if f.attname not in init_set
|
|
||||||
]
|
|
||||||
model_cls = deferred_class_factory(model_cls, skip)
|
|
||||||
return model_cls
|
|
||||||
|
|
||||||
def populate(self, row, from_obj):
|
def populate(self, row, from_obj):
|
||||||
if self.reorder_for_init:
|
if self.reorder_for_init:
|
||||||
obj_data = self.reorder_for_init(row)
|
obj_data = self.reorder_for_init(row)
|
||||||
|
|
|
@ -11,7 +11,6 @@ import inspect
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
from django.core.exceptions import FieldDoesNotExist
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
from django.db.backends import utils
|
|
||||||
from django.db.models.constants import LOOKUP_SEP
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
from django.utils import tree
|
from django.utils import tree
|
||||||
|
|
||||||
|
@ -97,10 +96,9 @@ class DeferredAttribute(object):
|
||||||
Retrieves and caches the value from the datastore on the first lookup.
|
Retrieves and caches the value from the datastore on the first lookup.
|
||||||
Returns the cached value.
|
Returns the cached value.
|
||||||
"""
|
"""
|
||||||
non_deferred_model = instance._meta.proxy_for_model
|
if instance is None:
|
||||||
opts = non_deferred_model._meta
|
return self
|
||||||
|
opts = instance._meta
|
||||||
assert instance is not None
|
|
||||||
data = instance.__dict__
|
data = instance.__dict__
|
||||||
if data.get(self.field_name, self) is self:
|
if data.get(self.field_name, self) is self:
|
||||||
# self.field_name is the attname of the field, but only() takes the
|
# self.field_name is the attname of the field, but only() takes the
|
||||||
|
@ -119,13 +117,6 @@ class DeferredAttribute(object):
|
||||||
data[self.field_name] = val
|
data[self.field_name] = val
|
||||||
return data[self.field_name]
|
return data[self.field_name]
|
||||||
|
|
||||||
def __set__(self, instance, value):
|
|
||||||
"""
|
|
||||||
Deferred loading attributes can be set normally (which means there will
|
|
||||||
never be a database lookup involved.
|
|
||||||
"""
|
|
||||||
instance.__dict__[self.field_name] = value
|
|
||||||
|
|
||||||
def _check_parent_chain(self, instance, name):
|
def _check_parent_chain(self, instance, name):
|
||||||
"""
|
"""
|
||||||
Check if the field value can be fetched from a parent field already
|
Check if the field value can be fetched from a parent field already
|
||||||
|
@ -230,48 +221,6 @@ def select_related_descend(field, restricted, requested, load_fields, reverse=Fa
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# This function is needed because data descriptors must be defined on a class
|
|
||||||
# object, not an instance, to have any effect.
|
|
||||||
|
|
||||||
def deferred_class_factory(model, attrs):
|
|
||||||
"""
|
|
||||||
Returns a class object that is a copy of "model" with the specified "attrs"
|
|
||||||
being replaced with DeferredAttribute objects. The "pk_value" ties the
|
|
||||||
deferred attributes to a particular instance of the model.
|
|
||||||
"""
|
|
||||||
if not attrs:
|
|
||||||
return model
|
|
||||||
opts = model._meta
|
|
||||||
# Never create deferred models based on deferred model
|
|
||||||
if model._deferred:
|
|
||||||
# Deferred models are proxies for the non-deferred model. We never
|
|
||||||
# create chains of defers => proxy_for_model is the non-deferred
|
|
||||||
# model.
|
|
||||||
model = opts.proxy_for_model
|
|
||||||
# The app registry wants a unique name for each model, otherwise the new
|
|
||||||
# class won't be created (we get an exception). Therefore, we generate
|
|
||||||
# the name using the passed in attrs. It's OK to reuse an existing class
|
|
||||||
# object if the attrs are identical.
|
|
||||||
name = "%s_Deferred_%s" % (model.__name__, '_'.join(sorted(attrs)))
|
|
||||||
name = utils.truncate_name(name, 80, 32)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return opts.apps.get_model(model._meta.app_label, name)
|
|
||||||
|
|
||||||
except LookupError:
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
proxy = True
|
|
||||||
apps = opts.apps
|
|
||||||
app_label = opts.app_label
|
|
||||||
|
|
||||||
overrides = {attr: DeferredAttribute(attr, model) for attr in attrs}
|
|
||||||
overrides["Meta"] = Meta
|
|
||||||
overrides["__module__"] = model.__module__
|
|
||||||
overrides["_deferred"] = True
|
|
||||||
return type(str(name), (model,), overrides)
|
|
||||||
|
|
||||||
|
|
||||||
def refs_expression(lookup_parts, annotations):
|
def refs_expression(lookup_parts, annotations):
|
||||||
"""
|
"""
|
||||||
A helper method to check if the lookup_parts contains references
|
A helper method to check if the lookup_parts contains references
|
||||||
|
|
|
@ -89,8 +89,6 @@ class SingleObjectMixin(ContextMixin):
|
||||||
if self.context_object_name:
|
if self.context_object_name:
|
||||||
return self.context_object_name
|
return self.context_object_name
|
||||||
elif isinstance(obj, models.Model):
|
elif isinstance(obj, models.Model):
|
||||||
if obj._deferred:
|
|
||||||
obj = obj._meta.proxy_for_model
|
|
||||||
return obj._meta.model_name
|
return obj._meta.model_name
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
@ -152,8 +150,6 @@ class SingleObjectTemplateResponseMixin(TemplateResponseMixin):
|
||||||
# only use this if the object in question is a model.
|
# only use this if the object in question is a model.
|
||||||
if isinstance(self.object, models.Model):
|
if isinstance(self.object, models.Model):
|
||||||
object_meta = self.object._meta
|
object_meta = self.object._meta
|
||||||
if self.object._deferred:
|
|
||||||
object_meta = self.object._meta.proxy_for_model._meta
|
|
||||||
names.append("%s/%s%s.html" % (
|
names.append("%s/%s%s.html" % (
|
||||||
object_meta.app_label,
|
object_meta.app_label,
|
||||||
object_meta.model_name,
|
object_meta.model_name,
|
||||||
|
|
|
@ -73,13 +73,12 @@ when loading from the database.
|
||||||
The ``db`` argument contains the database alias for the database the model
|
The ``db`` argument contains the database alias for the database the model
|
||||||
is loaded from, ``field_names`` contains the names of all loaded fields, and
|
is loaded from, ``field_names`` contains the names of all loaded fields, and
|
||||||
``values`` contains the loaded values for each field in ``field_names``. The
|
``values`` contains the loaded values for each field in ``field_names``. The
|
||||||
``field_names`` are in the same order as the ``values``, so it is possible to
|
``field_names`` are in the same order as the ``values``. If all of the model's
|
||||||
use ``cls(**(zip(field_names, values)))`` to instantiate the object. If all
|
fields are present, then ``values`` are guaranteed to be in the order
|
||||||
of the model's fields are present, then ``values`` are guaranteed to be in
|
``__init__()`` expects them. That is, the instance can be created by
|
||||||
the order ``__init__()`` expects them. That is, the instance can be created
|
``cls(*values)``. If any fields are deferred, they won't appear in
|
||||||
by ``cls(*values)``. It is possible to check if all fields are present by
|
``field_names``. In that case, assign a value of ``django.db.models.DEFERRED``
|
||||||
consulting ``cls._deferred`` - if ``False``, then all fields have been loaded
|
to each of the missing fields.
|
||||||
from the database.
|
|
||||||
|
|
||||||
In addition to creating the new model, the ``from_db()`` method must set the
|
In addition to creating the new model, the ``from_db()`` method must set the
|
||||||
``adding`` and ``db`` flags in the new instance's ``_state`` attribute.
|
``adding`` and ``db`` flags in the new instance's ``_state`` attribute.
|
||||||
|
@ -87,14 +86,20 @@ In addition to creating the new model, the ``from_db()`` method must set the
|
||||||
Below is an example showing how to record the initial values of fields that
|
Below is an example showing how to record the initial values of fields that
|
||||||
are loaded from the database::
|
are loaded from the database::
|
||||||
|
|
||||||
|
from django.db.models import DEFERRED
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_db(cls, db, field_names, values):
|
def from_db(cls, db, field_names, values):
|
||||||
# default implementation of from_db() (could be replaced
|
# Default implementation of from_db() (subject to change and could
|
||||||
# with super())
|
# be replaced with super()).
|
||||||
if cls._deferred:
|
if len(values) != len(cls._meta.concrete_fields):
|
||||||
instance = cls(**zip(field_names, values))
|
values = list(values)
|
||||||
else:
|
values.reverse()
|
||||||
instance = cls(*values)
|
values = [
|
||||||
|
values.pop() if f.attname in field_names else DEFERRED
|
||||||
|
for f in cls._meta.concrete_fields
|
||||||
|
]
|
||||||
|
new = cls(*values)
|
||||||
instance._state.adding = False
|
instance._state.adding = False
|
||||||
instance._state.db = db
|
instance._state.db = db
|
||||||
# customization to store the original field values on the instance
|
# customization to store the original field values on the instance
|
||||||
|
@ -114,6 +119,12 @@ The example above shows a full ``from_db()`` implementation to clarify how that
|
||||||
is done. In this case it would of course be possible to just use ``super()`` call
|
is done. In this case it would of course be possible to just use ``super()`` call
|
||||||
in the ``from_db()`` method.
|
in the ``from_db()`` method.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.10
|
||||||
|
|
||||||
|
In older versions, you could check if all fields were loaded by consulting
|
||||||
|
``cls._deferred``. This attribute is removed and
|
||||||
|
``django.db.models.DEFERRED`` is new.
|
||||||
|
|
||||||
Refreshing objects from database
|
Refreshing objects from database
|
||||||
================================
|
================================
|
||||||
|
|
||||||
|
|
|
@ -783,6 +783,12 @@ Miscellaneous
|
||||||
Web servers already implement this behavior. Responses retrieved using the
|
Web servers already implement this behavior. Responses retrieved using the
|
||||||
Django test client continue to have these "response fixes" applied.
|
Django test client continue to have these "response fixes" applied.
|
||||||
|
|
||||||
|
* ``Model.__init__()`` now receives ``django.db.models.DEFERRED`` as the value
|
||||||
|
of deferred fields.
|
||||||
|
|
||||||
|
* The ``Model._deferred`` attribute is removed as dynamic model classes when
|
||||||
|
using ``QuerySet.defer()`` and ``only()`` is removed.
|
||||||
|
|
||||||
.. _deprecated-features-1.10:
|
.. _deprecated-features-1.10:
|
||||||
|
|
||||||
Features deprecated in 1.10
|
Features deprecated in 1.10
|
||||||
|
|
|
@ -3650,7 +3650,7 @@ class AdminCustomQuerysetTest(TestCase):
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response,
|
response,
|
||||||
'<li class="success">The short message "<a href="%s">'
|
'<li class="success">The short message "<a href="%s">'
|
||||||
'ShortMessage_Deferred_timestamp object</a>" was changed successfully.</li>' %
|
'ShortMessage object</a>" was changed successfully.</li>' %
|
||||||
reverse('admin:admin_views_shortmessage_change', args=(sm.pk,)), html=True
|
reverse('admin:admin_views_shortmessage_change', args=(sm.pk,)), html=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -3697,7 +3697,7 @@ class AdminCustomQuerysetTest(TestCase):
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response,
|
response,
|
||||||
'<li class="success">The paper "<a href="%s">'
|
'<li class="success">The paper "<a href="%s">'
|
||||||
'Paper_Deferred_author object</a>" was changed successfully.</li>' %
|
'Paper object</a>" was changed successfully.</li>' %
|
||||||
reverse('admin:admin_views_paper_change', args=(p.pk,)), html=True
|
reverse('admin:admin_views_paper_change', args=(p.pk,)), html=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db.models.query_utils import DeferredAttribute, InvalidQuery
|
from django.db.models.query_utils import InvalidQuery
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
|
@ -15,10 +15,7 @@ class AssertionMixin(object):
|
||||||
we examine attribute values. Therefore, this method returns the number
|
we examine attribute values. Therefore, this method returns the number
|
||||||
of deferred fields on returned instances.
|
of deferred fields on returned instances.
|
||||||
"""
|
"""
|
||||||
count = 0
|
count = len(obj.get_deferred_fields())
|
||||||
for field in obj._meta.fields:
|
|
||||||
if isinstance(obj.__class__.__dict__.get(field.attname), DeferredAttribute):
|
|
||||||
count += 1
|
|
||||||
self.assertEqual(count, num)
|
self.assertEqual(count, num)
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,7 +42,9 @@ class DeferTests(AssertionMixin, TestCase):
|
||||||
# of them except the model's primary key see #15494
|
# of them except the model's primary key see #15494
|
||||||
self.assert_delayed(qs.only("pk")[0], 3)
|
self.assert_delayed(qs.only("pk")[0], 3)
|
||||||
# You can use 'pk' with reverse foreign key lookups.
|
# You can use 'pk' with reverse foreign key lookups.
|
||||||
self.assert_delayed(self.s1.primary_set.all().only('pk')[0], 3)
|
# The related_id is alawys set even if it's not fetched from the DB,
|
||||||
|
# so pk and related_id are not deferred.
|
||||||
|
self.assert_delayed(self.s1.primary_set.all().only('pk')[0], 2)
|
||||||
|
|
||||||
def test_defer_only_chaining(self):
|
def test_defer_only_chaining(self):
|
||||||
qs = Primary.objects.all()
|
qs = Primary.objects.all()
|
||||||
|
|
|
@ -2,16 +2,10 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.sessions.backends.db import SessionStore
|
from django.contrib.sessions.backends.db import SessionStore
|
||||||
from django.db import models
|
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.db.models.query_utils import (
|
|
||||||
DeferredAttribute, deferred_class_factory,
|
|
||||||
)
|
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.test.utils import isolate_apps
|
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Base, Child, Derived, Feature, Item, ItemAndSimpleItem, Leaf, Location,
|
Base, Child, Derived, Feature, Item, ItemAndSimpleItem, Leaf, Location,
|
||||||
|
@ -98,28 +92,6 @@ class DeferRegressionTest(TestCase):
|
||||||
list(SimpleItem.objects.annotate(Count('feature')).only('name')),
|
list(SimpleItem.objects.annotate(Count('feature')).only('name')),
|
||||||
list)
|
list)
|
||||||
|
|
||||||
def test_ticket_11936(self):
|
|
||||||
app_config = apps.get_app_config("defer_regress")
|
|
||||||
# Regression for #11936 - get_models should not return deferred models
|
|
||||||
# by default. Run a couple of defer queries so that app registry must
|
|
||||||
# contain some deferred classes. It might contain a lot more classes
|
|
||||||
# depending on the order the tests are ran.
|
|
||||||
list(Item.objects.defer("name"))
|
|
||||||
list(Child.objects.defer("value"))
|
|
||||||
klasses = {model.__name__ for model in app_config.get_models()}
|
|
||||||
self.assertIn("Child", klasses)
|
|
||||||
self.assertIn("Item", klasses)
|
|
||||||
self.assertNotIn("Child_Deferred_value", klasses)
|
|
||||||
self.assertNotIn("Item_Deferred_name", klasses)
|
|
||||||
self.assertFalse(any(k._deferred for k in app_config.get_models()))
|
|
||||||
|
|
||||||
klasses_with_deferred = {model.__name__ for model in app_config.get_models(include_deferred=True)}
|
|
||||||
self.assertIn("Child", klasses_with_deferred)
|
|
||||||
self.assertIn("Item", klasses_with_deferred)
|
|
||||||
self.assertIn("Child_Deferred_value", klasses_with_deferred)
|
|
||||||
self.assertIn("Item_Deferred_name", klasses_with_deferred)
|
|
||||||
self.assertTrue(any(k._deferred for k in app_config.get_models(include_deferred=True)))
|
|
||||||
|
|
||||||
@override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.PickleSerializer')
|
@override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.PickleSerializer')
|
||||||
def test_ticket_12163(self):
|
def test_ticket_12163(self):
|
||||||
# Test for #12163 - Pickling error saving session with unsaved model
|
# Test for #12163 - Pickling error saving session with unsaved model
|
||||||
|
@ -246,41 +218,6 @@ class DeferRegressionTest(TestCase):
|
||||||
qs = SpecialFeature.objects.only('feature__item__name').select_related('feature__item')
|
qs = SpecialFeature.objects.only('feature__item__name').select_related('feature__item')
|
||||||
self.assertEqual(len(qs), 1)
|
self.assertEqual(len(qs), 1)
|
||||||
|
|
||||||
def test_deferred_class_factory(self):
|
|
||||||
new_class = deferred_class_factory(
|
|
||||||
Item,
|
|
||||||
('this_is_some_very_long_attribute_name_so_modelname_truncation_is_triggered',))
|
|
||||||
self.assertEqual(
|
|
||||||
new_class.__name__,
|
|
||||||
'Item_Deferred_this_is_some_very_long_attribute_nac34b1f495507dad6b02e2cb235c875e')
|
|
||||||
|
|
||||||
def test_deferred_class_factory_already_deferred(self):
|
|
||||||
deferred_item1 = deferred_class_factory(Item, ('name',))
|
|
||||||
deferred_item2 = deferred_class_factory(deferred_item1, ('value',))
|
|
||||||
self.assertIs(deferred_item2._meta.proxy_for_model, Item)
|
|
||||||
self.assertNotIsInstance(deferred_item2.__dict__.get('name'), DeferredAttribute)
|
|
||||||
self.assertIsInstance(deferred_item2.__dict__.get('value'), DeferredAttribute)
|
|
||||||
|
|
||||||
def test_deferred_class_factory_no_attrs(self):
|
|
||||||
deferred_cls = deferred_class_factory(Item, ())
|
|
||||||
self.assertFalse(deferred_cls._deferred)
|
|
||||||
|
|
||||||
@isolate_apps('defer_regress', kwarg_name='apps')
|
|
||||||
def test_deferred_class_factory_apps_reuse(self, apps):
|
|
||||||
"""
|
|
||||||
#25563 - model._meta.apps should be used for caching and
|
|
||||||
retrieval of the created proxy class.
|
|
||||||
"""
|
|
||||||
class BaseModel(models.Model):
|
|
||||||
field = models.BooleanField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
app_label = 'defer_regress'
|
|
||||||
|
|
||||||
deferred_model = deferred_class_factory(BaseModel, ['field'])
|
|
||||||
self.assertIs(deferred_model._meta.apps, apps)
|
|
||||||
self.assertIs(deferred_class_factory(BaseModel, ['field']), deferred_model)
|
|
||||||
|
|
||||||
|
|
||||||
class DeferAnnotateSelectRelatedTest(TestCase):
|
class DeferAnnotateSelectRelatedTest(TestCase):
|
||||||
def test_defer_annotate_select_related(self):
|
def test_defer_annotate_select_related(self):
|
||||||
|
|
Loading…
Reference in New Issue