Fixed #24215 -- Refactored lazy model operations
This adds a new method, Apps.lazy_model_operation(), and a helper function, lazy_related_operation(), which together supersede add_lazy_relation() and make lazy model operations the responsibility of the App registry. This system no longer uses the class_prepared signal.
This commit is contained in:
parent
0f6f80c2e7
commit
720ff740e7
|
@ -2,6 +2,7 @@ import sys
|
||||||
import threading
|
import threading
|
||||||
import warnings
|
import warnings
|
||||||
from collections import Counter, OrderedDict, defaultdict
|
from collections import Counter, OrderedDict, defaultdict
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from django.core.exceptions import AppRegistryNotReady, ImproperlyConfigured
|
from django.core.exceptions import AppRegistryNotReady, ImproperlyConfigured
|
||||||
from django.utils import lru_cache
|
from django.utils import lru_cache
|
||||||
|
@ -45,8 +46,10 @@ class Apps(object):
|
||||||
# Lock for thread-safe population.
|
# Lock for thread-safe population.
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
# Pending lookups for lazy relations.
|
# Maps ("app_label", "modelname") tuples to lists of functions to be
|
||||||
self._pending_lookups = {}
|
# called when the corresponding model is ready. Used by this class's
|
||||||
|
# `lazy_model_operation()` and `do_pending_operations()` methods.
|
||||||
|
self._pending_operations = defaultdict(list)
|
||||||
|
|
||||||
# Populate apps and models, unless it's the master registry.
|
# Populate apps and models, unless it's the master registry.
|
||||||
if installed_apps is not None:
|
if installed_apps is not None:
|
||||||
|
@ -207,6 +210,7 @@ class Apps(object):
|
||||||
"Conflicting '%s' models in application '%s': %s and %s." %
|
"Conflicting '%s' models in application '%s': %s and %s." %
|
||||||
(model_name, app_label, app_models[model_name], model))
|
(model_name, app_label, app_models[model_name], model))
|
||||||
app_models[model_name] = model
|
app_models[model_name] = model
|
||||||
|
self.do_pending_operations(model)
|
||||||
self.clear_cache()
|
self.clear_cache()
|
||||||
|
|
||||||
def is_installed(self, app_name):
|
def is_installed(self, app_name):
|
||||||
|
@ -332,5 +336,42 @@ class Apps(object):
|
||||||
for model in app_config.get_models(include_auto_created=True):
|
for model in app_config.get_models(include_auto_created=True):
|
||||||
model._meta._expire_cache()
|
model._meta._expire_cache()
|
||||||
|
|
||||||
|
def lazy_model_operation(self, function, *model_keys):
|
||||||
|
"""
|
||||||
|
Take a function and a number of ("app_label", "modelname") tuples, and
|
||||||
|
when all the corresponding models have been imported and registered,
|
||||||
|
call the function with the model classes as its arguments.
|
||||||
|
|
||||||
|
The function passed to this method must accept exactly n models as
|
||||||
|
arguments, where n=len(model_keys).
|
||||||
|
"""
|
||||||
|
# If this function depends on more than one model, we recursively turn
|
||||||
|
# it into a chain of functions that accept a single model argument and
|
||||||
|
# pass each in turn to lazy_model_operation.
|
||||||
|
model_key, more_models = model_keys[0], model_keys[1:]
|
||||||
|
if more_models:
|
||||||
|
supplied_fn = function
|
||||||
|
|
||||||
|
def function(model):
|
||||||
|
next_function = partial(supplied_fn, model)
|
||||||
|
self.lazy_model_operation(next_function, *more_models)
|
||||||
|
|
||||||
|
# If the model is already loaded, pass it to the function immediately.
|
||||||
|
# Otherwise, delay execution until the class is prepared.
|
||||||
|
try:
|
||||||
|
model_class = self.get_registered_model(*model_key)
|
||||||
|
except LookupError:
|
||||||
|
self._pending_operations[model_key].append(function)
|
||||||
|
else:
|
||||||
|
function(model_class)
|
||||||
|
|
||||||
|
def do_pending_operations(self, model):
|
||||||
|
"""
|
||||||
|
Take a newly-prepared model and pass it to each function waiting for
|
||||||
|
it. This is called at the very end of `Apps.register_model()`.
|
||||||
|
"""
|
||||||
|
key = model._meta.app_label, model._meta.model_name
|
||||||
|
for function in self._pending_operations.pop(key, []):
|
||||||
|
function(model)
|
||||||
|
|
||||||
apps = Apps(installed_apps=None)
|
apps = Apps(installed_apps=None)
|
||||||
|
|
|
@ -8,10 +8,9 @@ from django.apps.registry import Apps, apps as global_apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.fields.proxy import OrderWrt
|
from django.db.models.fields.proxy import OrderWrt
|
||||||
from django.db.models.fields.related import (
|
from django.db.models.fields.related import RECURSIVE_RELATIONSHIP_CONSTANT
|
||||||
RECURSIVE_RELATIONSHIP_CONSTANT, do_pending_lookups,
|
|
||||||
)
|
|
||||||
from django.db.models.options import DEFAULT_NAMES, normalize_together
|
from django.db.models.options import DEFAULT_NAMES, normalize_together
|
||||||
|
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_text, smart_text
|
from django.utils.encoding import force_text, smart_text
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
@ -214,22 +213,21 @@ class StateApps(Apps):
|
||||||
self.render_multiple(list(models.values()) + self.real_models)
|
self.render_multiple(list(models.values()) + self.real_models)
|
||||||
|
|
||||||
# If there are some lookups left, see if we can first resolve them
|
# If there are some lookups left, see if we can first resolve them
|
||||||
# ourselves - sometimes fields are added after class_prepared is sent
|
# ourselves - sometimes fields are added after a model is registered
|
||||||
for lookup_model, operations in self._pending_lookups.items():
|
for lookup_model in self._pending_operations:
|
||||||
try:
|
try:
|
||||||
model = self.get_model(lookup_model[0], lookup_model[1])
|
model = self.get_model(*lookup_model)
|
||||||
except LookupError:
|
except LookupError:
|
||||||
app_label = "%s.%s" % (lookup_model[0], lookup_model[1])
|
if lookup_model == make_model_tuple(settings.AUTH_USER_MODEL) and ignore_swappable:
|
||||||
if app_label == settings.AUTH_USER_MODEL and ignore_swappable:
|
|
||||||
continue
|
continue
|
||||||
# Raise an error with a best-effort helpful message
|
# Raise an error with a best-effort helpful message
|
||||||
# (only for the first issue). Error message should look like:
|
# (only for the first issue). Error message should look like:
|
||||||
# "ValueError: Lookup failed for model referenced by
|
# "ValueError: Lookup failed for model referenced by
|
||||||
# field migrations.Book.author: migrations.Author"
|
# field migrations.Book.author: migrations.Author"
|
||||||
msg = "Lookup failed for model referenced by field {field}: {model[0]}.{model[1]}"
|
msg = "Lookup failed for model: {model[0]}.{model[1]}"
|
||||||
raise ValueError(msg.format(field=operations[0][1], model=lookup_model))
|
raise ValueError(msg.format(model=lookup_model))
|
||||||
else:
|
else:
|
||||||
do_pending_lookups(model)
|
self.do_pending_operations(model)
|
||||||
|
|
||||||
def render_multiple(self, model_states):
|
def render_multiple(self, model_states):
|
||||||
# We keep trying to render the models in a loop, ignoring invalid
|
# We keep trying to render the models in a loop, ignoring invalid
|
||||||
|
@ -277,6 +275,7 @@ class StateApps(Apps):
|
||||||
self.app_configs[app_label] = AppConfigStub(app_label)
|
self.app_configs[app_label] = AppConfigStub(app_label)
|
||||||
self.app_configs[app_label].models = OrderedDict()
|
self.app_configs[app_label].models = OrderedDict()
|
||||||
self.app_configs[app_label].models[model._meta.model_name] = model
|
self.app_configs[app_label].models[model._meta.model_name] = model
|
||||||
|
self.do_pending_operations(model)
|
||||||
self.clear_cache()
|
self.clear_cache()
|
||||||
|
|
||||||
def unregister_model(self, app_label, model_name):
|
def unregister_model(self, app_label, model_name):
|
||||||
|
|
|
@ -21,7 +21,8 @@ from django.db.models.constants import LOOKUP_SEP
|
||||||
from django.db.models.deletion import Collector
|
from django.db.models.deletion import Collector
|
||||||
from django.db.models.fields import AutoField
|
from django.db.models.fields import AutoField
|
||||||
from django.db.models.fields.related import (
|
from django.db.models.fields.related import (
|
||||||
ForeignObjectRel, ManyToOneRel, OneToOneField, add_lazy_relation,
|
ForeignObjectRel, ManyToOneRel, OneToOneField, lazy_related_operation,
|
||||||
|
resolve_relation,
|
||||||
)
|
)
|
||||||
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
|
||||||
|
@ -29,6 +30,7 @@ from django.db.models.query import Q
|
||||||
from django.db.models.query_utils import (
|
from django.db.models.query_utils import (
|
||||||
DeferredAttribute, deferred_class_factory,
|
DeferredAttribute, deferred_class_factory,
|
||||||
)
|
)
|
||||||
|
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
|
||||||
from django.utils.functional import curry
|
from django.utils.functional import curry
|
||||||
|
@ -199,8 +201,8 @@ class ModelBase(type):
|
||||||
# Locate OneToOneField instances.
|
# Locate OneToOneField instances.
|
||||||
for field in base._meta.local_fields:
|
for field in base._meta.local_fields:
|
||||||
if isinstance(field, OneToOneField):
|
if isinstance(field, OneToOneField):
|
||||||
parent_links[field.remote_field.model] = field
|
related = resolve_relation(new_class, field.remote_field.model)
|
||||||
|
parent_links[make_model_tuple(related)] = field
|
||||||
# Do the appropriate setup for any model parents.
|
# Do the appropriate setup for any model parents.
|
||||||
for base in parents:
|
for base in parents:
|
||||||
original_base = base
|
original_base = base
|
||||||
|
@ -223,8 +225,9 @@ class ModelBase(type):
|
||||||
if not base._meta.abstract:
|
if not base._meta.abstract:
|
||||||
# Concrete classes...
|
# Concrete classes...
|
||||||
base = base._meta.concrete_model
|
base = base._meta.concrete_model
|
||||||
if base in parent_links:
|
base_key = make_model_tuple(base)
|
||||||
field = parent_links[base]
|
if base_key in parent_links:
|
||||||
|
field = parent_links[base_key]
|
||||||
elif not is_proxy:
|
elif not is_proxy:
|
||||||
attr_name = '%s_ptr' % base._meta.model_name
|
attr_name = '%s_ptr' % base._meta.model_name
|
||||||
field = OneToOneField(base, name=attr_name,
|
field = OneToOneField(base, name=attr_name,
|
||||||
|
@ -305,7 +308,7 @@ class ModelBase(type):
|
||||||
|
|
||||||
# defer creating accessors on the foreign class until we are
|
# defer creating accessors on the foreign class until we are
|
||||||
# certain it has been created
|
# certain it has been created
|
||||||
def make_foreign_order_accessors(field, model, cls):
|
def make_foreign_order_accessors(cls, model, field):
|
||||||
setattr(
|
setattr(
|
||||||
field.remote_field.model,
|
field.remote_field.model,
|
||||||
'get_%s_order' % cls.__name__.lower(),
|
'get_%s_order' % cls.__name__.lower(),
|
||||||
|
@ -316,12 +319,8 @@ class ModelBase(type):
|
||||||
'set_%s_order' % cls.__name__.lower(),
|
'set_%s_order' % cls.__name__.lower(),
|
||||||
curry(method_set_order, cls)
|
curry(method_set_order, cls)
|
||||||
)
|
)
|
||||||
add_lazy_relation(
|
wrt = opts.order_with_respect_to
|
||||||
cls,
|
lazy_related_operation(make_foreign_order_accessors, cls, wrt.remote_field.model, field=wrt)
|
||||||
opts.order_with_respect_to,
|
|
||||||
opts.order_with_respect_to.remote_field.model,
|
|
||||||
make_foreign_order_accessors
|
|
||||||
)
|
|
||||||
|
|
||||||
# Give the class a docstring -- its definition.
|
# Give the class a docstring -- its definition.
|
||||||
if cls.__doc__ is None:
|
if cls.__doc__ is None:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
from functools import partial
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
@ -21,6 +22,7 @@ from django.db.models.fields.related_lookups import (
|
||||||
)
|
)
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.db.models.query_utils import PathInfo
|
from django.db.models.query_utils import PathInfo
|
||||||
|
from django.db.models.utils import make_model_tuple
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.deprecation import (
|
from django.utils.deprecation import (
|
||||||
RemovedInDjango20Warning, RemovedInDjango21Warning,
|
RemovedInDjango20Warning, RemovedInDjango21Warning,
|
||||||
|
@ -32,75 +34,60 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
RECURSIVE_RELATIONSHIP_CONSTANT = 'self'
|
RECURSIVE_RELATIONSHIP_CONSTANT = 'self'
|
||||||
|
|
||||||
|
|
||||||
def add_lazy_relation(cls, field, relation, operation):
|
def resolve_relation(scope_model, relation):
|
||||||
"""
|
"""
|
||||||
Adds a lookup on ``cls`` when a related field is defined using a string,
|
Transform relation into a model or fully-qualified model string of the form
|
||||||
i.e.::
|
"app_label.ModelName", relative to scope_model.
|
||||||
|
|
||||||
class MyModel(Model):
|
The relation argument can be:
|
||||||
fk = ForeignKey("AnotherModel")
|
* RECURSIVE_RELATIONSHIP_CONSTANT, i.e. the string "self", in which case
|
||||||
|
the model argument will be returned.
|
||||||
This string can be:
|
* A bare model name without an app_label, in which case scope_model's
|
||||||
|
app_label will be prepended.
|
||||||
* RECURSIVE_RELATIONSHIP_CONSTANT (i.e. "self") to indicate a recursive
|
* An "app_label.ModelName" string.
|
||||||
relation.
|
* A model class, which will be returned unchanged.
|
||||||
|
|
||||||
* The name of a model (i.e "AnotherModel") to indicate another model in
|
|
||||||
the same app.
|
|
||||||
|
|
||||||
* An app-label and model name (i.e. "someapp.AnotherModel") to indicate
|
|
||||||
another model in a different app.
|
|
||||||
|
|
||||||
If the other model hasn't yet been loaded -- almost a given if you're using
|
|
||||||
lazy relationships -- then the relation won't be set up until the
|
|
||||||
class_prepared signal fires at the end of model initialization.
|
|
||||||
|
|
||||||
``operation`` is the work that must be performed once the relation can be
|
|
||||||
resolved.
|
|
||||||
"""
|
"""
|
||||||
# Check for recursive relations
|
# Check for recursive relations
|
||||||
if relation == RECURSIVE_RELATIONSHIP_CONSTANT:
|
if relation == RECURSIVE_RELATIONSHIP_CONSTANT:
|
||||||
app_label = cls._meta.app_label
|
relation = scope_model
|
||||||
model_name = cls.__name__
|
|
||||||
|
|
||||||
else:
|
# Look for an "app.Model" relation
|
||||||
# Look for an "app.Model" relation.
|
if isinstance(relation, six.string_types):
|
||||||
|
if "." not in relation:
|
||||||
|
relation = "%s.%s" % (scope_model._meta.app_label, relation)
|
||||||
|
|
||||||
if isinstance(relation, six.string_types):
|
return relation
|
||||||
try:
|
|
||||||
app_label, model_name = relation.split(".")
|
|
||||||
except ValueError:
|
|
||||||
# If we can't split, assume a model in current app.
|
|
||||||
app_label = cls._meta.app_label
|
|
||||||
model_name = relation
|
|
||||||
else:
|
|
||||||
# It's actually a model class.
|
|
||||||
app_label = relation._meta.app_label
|
|
||||||
model_name = relation._meta.object_name
|
|
||||||
|
|
||||||
# Try to look up the related model, and if it's already loaded resolve the
|
|
||||||
# string right away. If get_registered_model raises a LookupError, it means
|
|
||||||
# that the related model isn't loaded yet, so we need to pend the relation
|
|
||||||
# until the class is prepared.
|
|
||||||
try:
|
|
||||||
model = cls._meta.apps.get_registered_model(app_label, model_name)
|
|
||||||
except LookupError:
|
|
||||||
key = (app_label, model_name)
|
|
||||||
value = (cls, field, operation)
|
|
||||||
cls._meta.apps._pending_lookups.setdefault(key, []).append(value)
|
|
||||||
else:
|
|
||||||
operation(field, model, cls)
|
|
||||||
|
|
||||||
|
|
||||||
def do_pending_lookups(sender, **kwargs):
|
def lazy_related_operation(function, model, *related_models, **kwargs):
|
||||||
"""
|
"""
|
||||||
Sent from class_prepared to handle pending relations to the sending model.
|
Schedule `function` to be called once `model` and all `related_models`
|
||||||
"""
|
have been imported and registered with the app registry. `function` will
|
||||||
key = (sender._meta.app_label, sender.__name__)
|
be called with the newly-loaded model classes as its positional arguments,
|
||||||
for cls, field, operation in sender._meta.apps._pending_lookups.pop(key, []):
|
plus any optional keyword arguments.
|
||||||
operation(field, sender, cls)
|
|
||||||
|
|
||||||
signals.class_prepared.connect(do_pending_lookups)
|
The `model` argument must be a model class. Each subsequent positional
|
||||||
|
argument is another model, or a reference to another model - see
|
||||||
|
`resolve_relation()` for the various forms these may take. Any relative
|
||||||
|
references will be resolved relative to `model`.
|
||||||
|
|
||||||
|
This is a convenience wrapper for `Apps.lazy_model_operation` - the app
|
||||||
|
registry model used is the one found in `model._meta.apps`.
|
||||||
|
"""
|
||||||
|
models = [model] + [resolve_relation(model, rel) for rel in related_models]
|
||||||
|
model_keys = (make_model_tuple(m) for m in models)
|
||||||
|
apps = model._meta.apps
|
||||||
|
return apps.lazy_model_operation(partial(function, **kwargs), *model_keys)
|
||||||
|
|
||||||
|
|
||||||
|
def add_lazy_relation(cls, field, relation, operation):
|
||||||
|
warnings.warn(
|
||||||
|
"add_lazy_relation() has been superseded by lazy_related_operation() "
|
||||||
|
"and related methods on the Apps class.",
|
||||||
|
RemovedInDjango21Warning, stacklevel=2)
|
||||||
|
# Rearrange args for new Apps.lazy_model_operation
|
||||||
|
function = lambda local, related, field: operation(field, related, local)
|
||||||
|
lazy_related_operation(function, cls, relation, field=field)
|
||||||
|
|
||||||
|
|
||||||
class RelatedField(Field):
|
class RelatedField(Field):
|
||||||
|
@ -289,13 +276,11 @@ class RelatedField(Field):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def contribute_to_class(self, cls, name, virtual_only=False):
|
def contribute_to_class(self, cls, name, virtual_only=False):
|
||||||
sup = super(RelatedField, self)
|
|
||||||
|
super(RelatedField, self).contribute_to_class(cls, name, virtual_only=virtual_only)
|
||||||
|
|
||||||
self.opts = cls._meta
|
self.opts = cls._meta
|
||||||
|
|
||||||
if hasattr(sup, 'contribute_to_class'):
|
|
||||||
sup.contribute_to_class(cls, name, virtual_only=virtual_only)
|
|
||||||
|
|
||||||
if not cls._meta.abstract:
|
if not cls._meta.abstract:
|
||||||
if self.remote_field.related_name:
|
if self.remote_field.related_name:
|
||||||
related_name = force_text(self.remote_field.related_name) % {
|
related_name = force_text(self.remote_field.related_name) % {
|
||||||
|
@ -303,14 +288,11 @@ class RelatedField(Field):
|
||||||
'app_label': cls._meta.app_label.lower()
|
'app_label': cls._meta.app_label.lower()
|
||||||
}
|
}
|
||||||
self.remote_field.related_name = related_name
|
self.remote_field.related_name = related_name
|
||||||
other = self.remote_field.model
|
|
||||||
if isinstance(other, six.string_types) or other._meta.pk is None:
|
def resolve_related_class(model, related, field):
|
||||||
def resolve_related_class(field, model, cls):
|
field.remote_field.model = related
|
||||||
field.remote_field.model = model
|
field.do_related_class(related, model)
|
||||||
field.do_related_class(model, cls)
|
lazy_related_operation(resolve_related_class, cls, self.remote_field.model, field=self)
|
||||||
add_lazy_relation(cls, self, other, resolve_related_class)
|
|
||||||
else:
|
|
||||||
self.do_related_class(other, cls)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def swappable_setting(self):
|
def swappable_setting(self):
|
||||||
|
@ -351,8 +333,7 @@ class RelatedField(Field):
|
||||||
|
|
||||||
def do_related_class(self, other, cls):
|
def do_related_class(self, other, cls):
|
||||||
self.set_attributes_from_rel()
|
self.set_attributes_from_rel()
|
||||||
if not cls._meta.abstract:
|
self.contribute_to_related_class(other, self.remote_field)
|
||||||
self.contribute_to_related_class(other, self.remote_field)
|
|
||||||
|
|
||||||
def get_limit_choices_to(self):
|
def get_limit_choices_to(self):
|
||||||
"""
|
"""
|
||||||
|
@ -2132,32 +2113,22 @@ class OneToOneField(ForeignKey):
|
||||||
|
|
||||||
def create_many_to_many_intermediary_model(field, klass):
|
def create_many_to_many_intermediary_model(field, klass):
|
||||||
from django.db import models
|
from django.db import models
|
||||||
managed = True
|
|
||||||
if isinstance(field.remote_field.model, six.string_types) and field.remote_field.model != RECURSIVE_RELATIONSHIP_CONSTANT:
|
|
||||||
to_model = field.remote_field.model
|
|
||||||
to = to_model.split('.')[-1]
|
|
||||||
|
|
||||||
def set_managed(field, model, cls):
|
def set_managed(model, related, through):
|
||||||
field.remote_field.through._meta.managed = model._meta.managed or cls._meta.managed
|
through._meta.managed = model._meta.managed or related._meta.managed
|
||||||
add_lazy_relation(klass, field, to_model, set_managed)
|
|
||||||
elif isinstance(field.remote_field.model, six.string_types):
|
to_model = resolve_relation(klass, field.remote_field.model)
|
||||||
to = klass._meta.object_name
|
|
||||||
to_model = klass
|
|
||||||
managed = klass._meta.managed
|
|
||||||
else:
|
|
||||||
to = field.remote_field.model._meta.object_name
|
|
||||||
to_model = field.remote_field.model
|
|
||||||
managed = klass._meta.managed or to_model._meta.managed
|
|
||||||
name = '%s_%s' % (klass._meta.object_name, field.name)
|
name = '%s_%s' % (klass._meta.object_name, field.name)
|
||||||
if field.remote_field.model == RECURSIVE_RELATIONSHIP_CONSTANT or to == klass._meta.object_name:
|
lazy_related_operation(set_managed, klass, to_model, name)
|
||||||
from_ = 'from_%s' % to.lower()
|
|
||||||
to = 'to_%s' % to.lower()
|
to = make_model_tuple(to_model)[1]
|
||||||
else:
|
from_ = klass._meta.model_name
|
||||||
from_ = klass._meta.model_name
|
if to == from_:
|
||||||
to = to.lower()
|
to = 'to_%s' % to
|
||||||
|
from_ = 'from_%s' % from_
|
||||||
|
|
||||||
meta = type(str('Meta'), (object,), {
|
meta = type(str('Meta'), (object,), {
|
||||||
'db_table': field._get_m2m_db_table(klass._meta),
|
'db_table': field._get_m2m_db_table(klass._meta),
|
||||||
'managed': managed,
|
|
||||||
'auto_created': klass,
|
'auto_created': klass,
|
||||||
'app_label': klass._meta.app_label,
|
'app_label': klass._meta.app_label,
|
||||||
'db_tablespace': klass._meta.db_tablespace,
|
'db_tablespace': klass._meta.db_tablespace,
|
||||||
|
@ -2323,7 +2294,7 @@ class ManyToManyField(RelatedField):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set some useful local variables
|
# Set some useful local variables
|
||||||
to_model = self.remote_field.model
|
to_model = resolve_relation(from_model, self.remote_field.model)
|
||||||
from_model_name = from_model._meta.object_name
|
from_model_name = from_model._meta.object_name
|
||||||
if isinstance(to_model, six.string_types):
|
if isinstance(to_model, six.string_types):
|
||||||
to_model_name = to_model
|
to_model_name = to_model
|
||||||
|
@ -2657,8 +2628,13 @@ class ManyToManyField(RelatedField):
|
||||||
# 1) There is a manually specified intermediate, or
|
# 1) There is a manually specified intermediate, or
|
||||||
# 2) The class owning the m2m field is abstract.
|
# 2) The class owning the m2m field is abstract.
|
||||||
# 3) The class owning the m2m field has been swapped out.
|
# 3) The class owning the m2m field has been swapped out.
|
||||||
if not self.remote_field.through and not cls._meta.abstract and not cls._meta.swapped:
|
if not cls._meta.abstract:
|
||||||
self.remote_field.through = create_many_to_many_intermediary_model(self, cls)
|
if self.remote_field.through:
|
||||||
|
def resolve_through_model(_, model, field):
|
||||||
|
field.remote_field.through = model
|
||||||
|
lazy_related_operation(resolve_through_model, cls, self.remote_field.through, field=self)
|
||||||
|
elif not cls._meta.swapped:
|
||||||
|
self.remote_field.through = create_many_to_many_intermediary_model(self, cls)
|
||||||
|
|
||||||
# Add the descriptor for the m2m relation.
|
# Add the descriptor for the m2m relation.
|
||||||
setattr(cls, self.name, ManyRelatedObjectsDescriptor(self.remote_field, reverse=False))
|
setattr(cls, self.name, ManyRelatedObjectsDescriptor(self.remote_field, reverse=False))
|
||||||
|
@ -2666,13 +2642,6 @@ class ManyToManyField(RelatedField):
|
||||||
# Set up the accessor for the m2m table name for the relation.
|
# Set up the accessor for the m2m table name for the relation.
|
||||||
self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta)
|
self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta)
|
||||||
|
|
||||||
# Populate some necessary rel arguments so that cross-app relations
|
|
||||||
# work correctly.
|
|
||||||
if not cls._meta.abstract and isinstance(self.remote_field.through, six.string_types):
|
|
||||||
def resolve_through_model(field, model, cls):
|
|
||||||
field.remote_field.through = model
|
|
||||||
add_lazy_relation(cls, self, self.remote_field.through, resolve_through_model)
|
|
||||||
|
|
||||||
def contribute_to_related_class(self, cls, related):
|
def contribute_to_related_class(self, cls, related):
|
||||||
# Internal M2Ms (i.e., those with a related name ending with '+')
|
# Internal M2Ms (i.e., those with a related name ending with '+')
|
||||||
# and swapped models don't get a related descriptor.
|
# and swapped models don't get a related descriptor.
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
|
|
||||||
|
def make_model_tuple(model):
|
||||||
|
"""
|
||||||
|
Takes a model or a string of the form "app_label.ModelName" and returns a
|
||||||
|
corresponding ("app_label", "modelname") tuple. If a tuple is passed in,
|
||||||
|
it's assumed to be a valid model tuple already and returned unchanged.
|
||||||
|
"""
|
||||||
|
if isinstance(model, tuple):
|
||||||
|
model_tuple = model
|
||||||
|
elif isinstance(model, six.string_types):
|
||||||
|
app_label, model_name = model.split(".")
|
||||||
|
model_tuple = app_label, model_name.lower()
|
||||||
|
else:
|
||||||
|
model_tuple = model._meta.app_label, model._meta.model_name
|
||||||
|
assert len(model_tuple) == 2, "Invalid model representation: %s" % model
|
||||||
|
return model_tuple
|
|
@ -30,6 +30,8 @@ details on these changes.
|
||||||
|
|
||||||
* ``Field.remote_field.to`` attribute will be removed.
|
* ``Field.remote_field.to`` attribute will be removed.
|
||||||
|
|
||||||
|
* ``django.db.models.fields.add_lazy_relation`` will be removed.
|
||||||
|
|
||||||
.. _deprecation-removed-in-2.0:
|
.. _deprecation-removed-in-2.0:
|
||||||
|
|
||||||
2.0
|
2.0
|
||||||
|
|
|
@ -372,6 +372,8 @@ Miscellaneous
|
||||||
:class:`~django.forms.SelectDateWidget` in ``django.forms.widgets``
|
:class:`~django.forms.SelectDateWidget` in ``django.forms.widgets``
|
||||||
(or simply ``django.forms``) instead.
|
(or simply ``django.forms``) instead.
|
||||||
|
|
||||||
|
* Private API ``django.db.models.fields.add_lazy_relation()`` is deprecated.
|
||||||
|
|
||||||
.. removed-features-1.9:
|
.. removed-features-1.9:
|
||||||
|
|
||||||
Features removed in 1.9
|
Features removed in 1.9
|
||||||
|
|
|
@ -259,6 +259,44 @@ class AppsTests(TestCase):
|
||||||
finally:
|
finally:
|
||||||
apps.apps_ready = True
|
apps.apps_ready = True
|
||||||
|
|
||||||
|
def test_lazy_model_operation(self):
|
||||||
|
"""
|
||||||
|
Tests apps.lazy_model_operation().
|
||||||
|
"""
|
||||||
|
model_classes = []
|
||||||
|
initial_pending = set(apps._pending_operations)
|
||||||
|
|
||||||
|
def test_func(*models):
|
||||||
|
model_classes[:] = models
|
||||||
|
|
||||||
|
class LazyA(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Test models appearing twice, and models appearing consecutively
|
||||||
|
model_keys = [('apps', model_name) for model_name in ['lazya', 'lazyb', 'lazyb', 'lazyc', 'lazya']]
|
||||||
|
apps.lazy_model_operation(test_func, *model_keys)
|
||||||
|
|
||||||
|
# LazyModelA shouldn't be waited on since it's already registered,
|
||||||
|
# and LazyModelC shouldn't be waited on until LazyModelB exists.
|
||||||
|
self.assertSetEqual(set(apps._pending_operations) - initial_pending, {('apps', 'lazyb')})
|
||||||
|
|
||||||
|
# Test that multiple operations can wait on the same model
|
||||||
|
apps.lazy_model_operation(test_func, ('apps', 'lazyb'))
|
||||||
|
|
||||||
|
class LazyB(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.assertListEqual(model_classes, [LazyB])
|
||||||
|
|
||||||
|
# Now we are just waiting on LazyModelC.
|
||||||
|
self.assertSetEqual(set(apps._pending_operations) - initial_pending, {('apps', 'lazyc')})
|
||||||
|
|
||||||
|
class LazyC(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Everything should be loaded - make sure the callback was executed properly.
|
||||||
|
self.assertListEqual(model_classes, [LazyA, LazyB, LazyB, LazyC, LazyA])
|
||||||
|
|
||||||
|
|
||||||
class Stub(object):
|
class Stub(object):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
|
|
@ -373,14 +373,3 @@ class PrimaryKeyUUIDModel(models.Model):
|
||||||
|
|
||||||
class RelatedToUUIDModel(models.Model):
|
class RelatedToUUIDModel(models.Model):
|
||||||
uuid_fk = models.ForeignKey('PrimaryKeyUUIDModel')
|
uuid_fk = models.ForeignKey('PrimaryKeyUUIDModel')
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
# See ticket #24215.
|
|
||||||
class AbstractForeignFieldsModel(models.Model):
|
|
||||||
fk = models.ForeignKey('missing.FK')
|
|
||||||
m2m = models.ManyToManyField('missing.M2M', through='missing.Through')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import unittest
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django import forms, test
|
from django import forms, test
|
||||||
|
from django.apps import apps
|
||||||
from django.core import checks, validators
|
from django.core import checks, validators
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import IntegrityError, connection, models, transaction
|
from django.db import IntegrityError, connection, models, transaction
|
||||||
|
@ -21,12 +22,11 @@ from django.utils import six
|
||||||
from django.utils.functional import lazy
|
from django.utils.functional import lazy
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
AbstractForeignFieldsModel, Bar, BigD, BigIntegerModel, BigS, BooleanModel,
|
Bar, BigD, BigIntegerModel, BigS, BooleanModel, DataModel, DateTimeModel,
|
||||||
DataModel, DateTimeModel, Document, FksToBooleans, FkToChar, FloatModel,
|
Document, FksToBooleans, FkToChar, FloatModel, Foo, GenericIPAddress,
|
||||||
Foo, GenericIPAddress, IntegerModel, NullBooleanModel,
|
IntegerModel, NullBooleanModel, PositiveIntegerModel,
|
||||||
PositiveIntegerModel, PositiveSmallIntegerModel, Post, PrimaryKeyCharModel,
|
PositiveSmallIntegerModel, Post, PrimaryKeyCharModel, RenamedField,
|
||||||
RenamedField, SmallIntegerModel, VerboseNameField, Whiz, WhizIter,
|
SmallIntegerModel, VerboseNameField, Whiz, WhizIter, WhizIterEmpty,
|
||||||
WhizIterEmpty,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -202,37 +202,46 @@ class ForeignKeyTests(test.TestCase):
|
||||||
rel_name = Bar._meta.get_field('a').remote_field.related_name
|
rel_name = Bar._meta.get_field('a').remote_field.related_name
|
||||||
self.assertIsInstance(rel_name, six.text_type)
|
self.assertIsInstance(rel_name, six.text_type)
|
||||||
|
|
||||||
def test_abstract_model_pending_lookups(self):
|
def test_abstract_model_pending_operations(self):
|
||||||
"""
|
"""
|
||||||
Foreign key fields declared on abstract models should not add lazy relations to
|
Foreign key fields declared on abstract models should not add lazy relations to
|
||||||
resolve relationship declared as string. refs #24215
|
resolve relationship declared as string. refs #24215
|
||||||
"""
|
"""
|
||||||
opts = AbstractForeignFieldsModel._meta
|
pending_ops_before = list(apps._pending_operations.items())
|
||||||
to_key = ('missing', 'FK')
|
|
||||||
fk_lookup = (AbstractForeignFieldsModel, opts.get_field('fk'))
|
class AbstractForeignKeyModel(models.Model):
|
||||||
self.assertFalse(
|
fk = models.ForeignKey('missing.FK')
|
||||||
any(lookup[0:2] == fk_lookup for lookup in opts.apps._pending_lookups.get(to_key, [])),
|
|
||||||
'Pending lookup added for the abstract model foreign key `to` parameter'
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
self.assertIs(AbstractForeignKeyModel._meta.apps, apps)
|
||||||
|
self.assertEqual(
|
||||||
|
pending_ops_before,
|
||||||
|
list(apps._pending_operations.items()),
|
||||||
|
"Pending lookup added for a foreign key on an abstract model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ManyToManyFieldTests(test.TestCase):
|
class ManyToManyFieldTests(test.TestCase):
|
||||||
def test_abstract_model_pending_lookups(self):
|
def test_abstract_model_pending_operations(self):
|
||||||
"""
|
"""
|
||||||
Many-to-many fields declared on abstract models should not add lazy relations to
|
Many-to-many fields declared on abstract models should not add lazy relations to
|
||||||
resolve relationship declared as string. refs #24215
|
resolve relationship declared as string. refs #24215
|
||||||
"""
|
"""
|
||||||
opts = AbstractForeignFieldsModel._meta
|
pending_ops_before = list(apps._pending_operations.items())
|
||||||
to_key = ('missing', 'M2M')
|
|
||||||
fk_lookup = (AbstractForeignFieldsModel, opts.get_field('m2m'))
|
class AbstractManyToManyModel(models.Model):
|
||||||
self.assertFalse(
|
fk = models.ForeignKey('missing.FK')
|
||||||
any(lookup[0:2] == fk_lookup for lookup in opts.apps._pending_lookups.get(to_key, [])),
|
|
||||||
'Pending lookup added for the abstract model many-to-many `to` parameter.'
|
class Meta:
|
||||||
)
|
abstract = True
|
||||||
through_key = ('missing', 'Through')
|
|
||||||
self.assertFalse(
|
self.assertIs(AbstractManyToManyModel._meta.apps, apps)
|
||||||
any(lookup[0:2] == fk_lookup for lookup in opts.apps._pending_lookups.get(through_key, [])),
|
self.assertEqual(
|
||||||
'Pending lookup added for the abstract model many-to-many `through` parameter.'
|
pending_ops_before,
|
||||||
|
list(apps._pending_operations.items()),
|
||||||
|
"Pending lookup added for a many-to-many field on an abstract model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue