Fixed #26207 -- Replaced dynamic classes with non-data descriptors for deferred instance loading.

This commit is contained in:
Anssi Kääriäinen 2016-02-02 11:33:09 +02:00 committed by Tim Graham
parent dac075e910
commit 7f51876f99
17 changed files with 104 additions and 240 deletions

View File

@ -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:

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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,

View File

@ -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

View File

@ -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))

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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
================================ ================================

View File

@ -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

View File

@ -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
) )

View File

@ -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()

View File

@ -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):