summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Hill <alex@hill.net.au>2015-03-03 16:43:56 +0800
committerTim Graham <timograham@gmail.com>2015-03-25 16:48:17 -0400
commit720ff740e70e649a97fcf0232fec132569a52c7e (patch)
tree688533ec18f1d970b8c7671ef94be83b67720b80
parent0f6f80c2e7736ec4e2aa40287fe8c37ffff0a783 (diff)
downloaddjango-720ff740e70e649a97fcf0232fec132569a52c7e.tar.gz
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.
-rw-r--r--django/apps/registry.py45
-rw-r--r--django/db/migrations/state.py21
-rw-r--r--django/db/models/base.py23
-rw-r--r--django/db/models/fields/related.py183
-rw-r--r--django/db/models/utils.py18
-rw-r--r--docs/internals/deprecation.txt2
-rw-r--r--docs/releases/1.9.txt2
-rw-r--r--tests/apps/tests.py38
-rw-r--r--tests/model_fields/models.py11
-rw-r--r--tests/model_fields/tests.py59
10 files changed, 234 insertions, 168 deletions
diff --git a/django/apps/registry.py b/django/apps/registry.py
index 164427d85f..96d4db5f17 100644
--- a/django/apps/registry.py
+++ b/django/apps/registry.py
@@ -2,6 +2,7 @@ import sys
import threading
import warnings
from collections import Counter, OrderedDict, defaultdict
+from functools import partial
from django.core.exceptions import AppRegistryNotReady, ImproperlyConfigured
from django.utils import lru_cache
@@ -45,8 +46,10 @@ class Apps(object):
# Lock for thread-safe population.
self._lock = threading.Lock()
- # Pending lookups for lazy relations.
- self._pending_lookups = {}
+ # Maps ("app_label", "modelname") tuples to lists of functions to be
+ # 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.
if installed_apps is not None:
@@ -207,6 +210,7 @@ class Apps(object):
"Conflicting '%s' models in application '%s': %s and %s." %
(model_name, app_label, app_models[model_name], model))
app_models[model_name] = model
+ self.do_pending_operations(model)
self.clear_cache()
def is_installed(self, app_name):
@@ -332,5 +336,42 @@ class Apps(object):
for model in app_config.get_models(include_auto_created=True):
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)
diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py
index d0d065af8a..0482a680da 100644
--- a/django/db/migrations/state.py
+++ b/django/db/migrations/state.py
@@ -8,10 +8,9 @@ from django.apps.registry import Apps, apps as global_apps
from django.conf import settings
from django.db import models
from django.db.models.fields.proxy import OrderWrt
-from django.db.models.fields.related import (
- RECURSIVE_RELATIONSHIP_CONSTANT, do_pending_lookups,
-)
+from django.db.models.fields.related import RECURSIVE_RELATIONSHIP_CONSTANT
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.encoding import force_text, smart_text
from django.utils.functional import cached_property
@@ -214,22 +213,21 @@ class StateApps(Apps):
self.render_multiple(list(models.values()) + self.real_models)
# If there are some lookups left, see if we can first resolve them
- # ourselves - sometimes fields are added after class_prepared is sent
- for lookup_model, operations in self._pending_lookups.items():
+ # ourselves - sometimes fields are added after a model is registered
+ for lookup_model in self._pending_operations:
try:
- model = self.get_model(lookup_model[0], lookup_model[1])
+ model = self.get_model(*lookup_model)
except LookupError:
- app_label = "%s.%s" % (lookup_model[0], lookup_model[1])
- if app_label == settings.AUTH_USER_MODEL and ignore_swappable:
+ if lookup_model == make_model_tuple(settings.AUTH_USER_MODEL) and ignore_swappable:
continue
# Raise an error with a best-effort helpful message
# (only for the first issue). Error message should look like:
# "ValueError: Lookup failed for model referenced by
# field migrations.Book.author: migrations.Author"
- msg = "Lookup failed for model referenced by field {field}: {model[0]}.{model[1]}"
- raise ValueError(msg.format(field=operations[0][1], model=lookup_model))
+ msg = "Lookup failed for model: {model[0]}.{model[1]}"
+ raise ValueError(msg.format(model=lookup_model))
else:
- do_pending_lookups(model)
+ self.do_pending_operations(model)
def render_multiple(self, model_states):
# 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].models = OrderedDict()
self.app_configs[app_label].models[model._meta.model_name] = model
+ self.do_pending_operations(model)
self.clear_cache()
def unregister_model(self, app_label, model_name):
diff --git a/django/db/models/base.py b/django/db/models/base.py
index fdd1ce28d9..b5d140393f 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -21,7 +21,8 @@ from django.db.models.constants import LOOKUP_SEP
from django.db.models.deletion import Collector
from django.db.models.fields import AutoField
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.options import Options
@@ -29,6 +30,7 @@ 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.utils import six
from django.utils.encoding import force_str, force_text
from django.utils.functional import curry
@@ -199,8 +201,8 @@ class ModelBase(type):
# Locate OneToOneField instances.
for field in base._meta.local_fields:
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.
for base in parents:
original_base = base
@@ -223,8 +225,9 @@ class ModelBase(type):
if not base._meta.abstract:
# Concrete classes...
base = base._meta.concrete_model
- if base in parent_links:
- field = parent_links[base]
+ base_key = make_model_tuple(base)
+ if base_key in parent_links:
+ field = parent_links[base_key]
elif not is_proxy:
attr_name = '%s_ptr' % base._meta.model_name
field = OneToOneField(base, name=attr_name,
@@ -305,7 +308,7 @@ class ModelBase(type):
# defer creating accessors on the foreign class until we are
# certain it has been created
- def make_foreign_order_accessors(field, model, cls):
+ def make_foreign_order_accessors(cls, model, field):
setattr(
field.remote_field.model,
'get_%s_order' % cls.__name__.lower(),
@@ -316,12 +319,8 @@ class ModelBase(type):
'set_%s_order' % cls.__name__.lower(),
curry(method_set_order, cls)
)
- add_lazy_relation(
- cls,
- opts.order_with_respect_to,
- opts.order_with_respect_to.remote_field.model,
- make_foreign_order_accessors
- )
+ wrt = opts.order_with_respect_to
+ lazy_related_operation(make_foreign_order_accessors, cls, wrt.remote_field.model, field=wrt)
# Give the class a docstring -- its definition.
if cls.__doc__ is None:
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index 9e6bbaaf29..cd535f2c4e 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -1,6 +1,7 @@
from __future__ import unicode_literals
import warnings
+from functools import partial
from operator import attrgetter
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_utils import PathInfo
+from django.db.models.utils import make_model_tuple
from django.utils import six
from django.utils.deprecation import (
RemovedInDjango20Warning, RemovedInDjango21Warning,
@@ -32,75 +34,60 @@ from django.utils.translation import ugettext_lazy as _
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,
- i.e.::
-
- class MyModel(Model):
- fk = ForeignKey("AnotherModel")
-
- This string can be:
-
- * RECURSIVE_RELATIONSHIP_CONSTANT (i.e. "self") to indicate a recursive
- relation.
-
- * 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.
+ Transform relation into a model or fully-qualified model string of the form
+ "app_label.ModelName", relative to scope_model.
+
+ The relation argument can be:
+ * RECURSIVE_RELATIONSHIP_CONSTANT, i.e. the string "self", in which case
+ the model argument will be returned.
+ * A bare model name without an app_label, in which case scope_model's
+ app_label will be prepended.
+ * An "app_label.ModelName" string.
+ * A model class, which will be returned unchanged.
"""
# Check for recursive relations
if relation == RECURSIVE_RELATIONSHIP_CONSTANT:
- app_label = cls._meta.app_label
- model_name = cls.__name__
+ relation = scope_model
- 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):
- 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):
+ return relation
+
+
+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
+ be called with the newly-loaded model classes as its positional arguments,
+ plus any optional keyword arguments.
+
+ 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`.
"""
- key = (sender._meta.app_label, sender.__name__)
- for cls, field, operation in sender._meta.apps._pending_lookups.pop(key, []):
- operation(field, sender, cls)
+ 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)
-signals.class_prepared.connect(do_pending_lookups)
+
+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):
@@ -289,12 +276,10 @@ class RelatedField(Field):
return None
def contribute_to_class(self, cls, name, virtual_only=False):
- sup = super(RelatedField, self)
- self.opts = cls._meta
+ super(RelatedField, self).contribute_to_class(cls, name, virtual_only=virtual_only)
- if hasattr(sup, 'contribute_to_class'):
- sup.contribute_to_class(cls, name, virtual_only=virtual_only)
+ self.opts = cls._meta
if not cls._meta.abstract:
if self.remote_field.related_name:
@@ -303,14 +288,11 @@ class RelatedField(Field):
'app_label': cls._meta.app_label.lower()
}
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(field, model, cls):
- field.remote_field.model = model
- field.do_related_class(model, cls)
- add_lazy_relation(cls, self, other, resolve_related_class)
- else:
- self.do_related_class(other, cls)
+
+ def resolve_related_class(model, related, field):
+ field.remote_field.model = related
+ field.do_related_class(related, model)
+ lazy_related_operation(resolve_related_class, cls, self.remote_field.model, field=self)
@property
def swappable_setting(self):
@@ -351,8 +333,7 @@ class RelatedField(Field):
def do_related_class(self, other, cls):
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):
"""
@@ -2132,32 +2113,22 @@ class OneToOneField(ForeignKey):
def create_many_to_many_intermediary_model(field, klass):
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):
- field.remote_field.through._meta.managed = model._meta.managed or cls._meta.managed
- add_lazy_relation(klass, field, to_model, set_managed)
- elif isinstance(field.remote_field.model, six.string_types):
- 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
+
+ def set_managed(model, related, through):
+ through._meta.managed = model._meta.managed or related._meta.managed
+
+ to_model = resolve_relation(klass, field.remote_field.model)
name = '%s_%s' % (klass._meta.object_name, field.name)
- if field.remote_field.model == RECURSIVE_RELATIONSHIP_CONSTANT or to == klass._meta.object_name:
- from_ = 'from_%s' % to.lower()
- to = 'to_%s' % to.lower()
- else:
- from_ = klass._meta.model_name
- to = to.lower()
+ lazy_related_operation(set_managed, klass, to_model, name)
+
+ to = make_model_tuple(to_model)[1]
+ from_ = klass._meta.model_name
+ if to == from_:
+ to = 'to_%s' % to
+ from_ = 'from_%s' % from_
+
meta = type(str('Meta'), (object,), {
'db_table': field._get_m2m_db_table(klass._meta),
- 'managed': managed,
'auto_created': klass,
'app_label': klass._meta.app_label,
'db_tablespace': klass._meta.db_tablespace,
@@ -2323,7 +2294,7 @@ class ManyToManyField(RelatedField):
)
# 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
if isinstance(to_model, six.string_types):
to_model_name = to_model
@@ -2657,8 +2628,13 @@ class ManyToManyField(RelatedField):
# 1) There is a manually specified intermediate, or
# 2) The class owning the m2m field is abstract.
# 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:
- self.remote_field.through = create_many_to_many_intermediary_model(self, cls)
+ if not cls._meta.abstract:
+ 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.
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.
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):
# Internal M2Ms (i.e., those with a related name ending with '+')
# and swapped models don't get a related descriptor.
diff --git a/django/db/models/utils.py b/django/db/models/utils.py
new file mode 100644
index 0000000000..f696e30ce2
--- /dev/null
+++ b/django/db/models/utils.py
@@ -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
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index abce521d75..494f482828 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -30,6 +30,8 @@ details on these changes.
* ``Field.remote_field.to`` attribute will be removed.
+* ``django.db.models.fields.add_lazy_relation`` will be removed.
+
.. _deprecation-removed-in-2.0:
2.0
diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt
index 9f15e9151a..3626cd0a40 100644
--- a/docs/releases/1.9.txt
+++ b/docs/releases/1.9.txt
@@ -372,6 +372,8 @@ Miscellaneous
:class:`~django.forms.SelectDateWidget` in ``django.forms.widgets``
(or simply ``django.forms``) instead.
+* Private API ``django.db.models.fields.add_lazy_relation()`` is deprecated.
+
.. removed-features-1.9:
Features removed in 1.9
diff --git a/tests/apps/tests.py b/tests/apps/tests.py
index 89e295ffde..347996454f 100644
--- a/tests/apps/tests.py
+++ b/tests/apps/tests.py
@@ -259,6 +259,44 @@ class AppsTests(TestCase):
finally:
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):
def __init__(self, **kwargs):
diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py
index 4208454b68..cca2b463a0 100644
--- a/tests/model_fields/models.py
+++ b/tests/model_fields/models.py
@@ -373,14 +373,3 @@ class PrimaryKeyUUIDModel(models.Model):
class RelatedToUUIDModel(models.Model):
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
diff --git a/tests/model_fields/tests.py b/tests/model_fields/tests.py
index 31f2cee337..6b63b8f115 100644
--- a/tests/model_fields/tests.py
+++ b/tests/model_fields/tests.py
@@ -5,6 +5,7 @@ import unittest
from decimal import Decimal
from django import forms, test
+from django.apps import apps
from django.core import checks, validators
from django.core.exceptions import ValidationError
from django.db import IntegrityError, connection, models, transaction
@@ -21,12 +22,11 @@ from django.utils import six
from django.utils.functional import lazy
from .models import (
- AbstractForeignFieldsModel, Bar, BigD, BigIntegerModel, BigS, BooleanModel,
- DataModel, DateTimeModel, Document, FksToBooleans, FkToChar, FloatModel,
- Foo, GenericIPAddress, IntegerModel, NullBooleanModel,
- PositiveIntegerModel, PositiveSmallIntegerModel, Post, PrimaryKeyCharModel,
- RenamedField, SmallIntegerModel, VerboseNameField, Whiz, WhizIter,
- WhizIterEmpty,
+ Bar, BigD, BigIntegerModel, BigS, BooleanModel, DataModel, DateTimeModel,
+ Document, FksToBooleans, FkToChar, FloatModel, Foo, GenericIPAddress,
+ IntegerModel, NullBooleanModel, PositiveIntegerModel,
+ PositiveSmallIntegerModel, Post, PrimaryKeyCharModel, RenamedField,
+ SmallIntegerModel, VerboseNameField, Whiz, WhizIter, WhizIterEmpty,
)
@@ -202,37 +202,46 @@ class ForeignKeyTests(test.TestCase):
rel_name = Bar._meta.get_field('a').remote_field.related_name
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
resolve relationship declared as string. refs #24215
"""
- opts = AbstractForeignFieldsModel._meta
- to_key = ('missing', 'FK')
- fk_lookup = (AbstractForeignFieldsModel, opts.get_field('fk'))
- self.assertFalse(
- 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'
+ pending_ops_before = list(apps._pending_operations.items())
+
+ class AbstractForeignKeyModel(models.Model):
+ fk = models.ForeignKey('missing.FK')
+
+ 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):
- 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
resolve relationship declared as string. refs #24215
"""
- opts = AbstractForeignFieldsModel._meta
- to_key = ('missing', 'M2M')
- fk_lookup = (AbstractForeignFieldsModel, opts.get_field('m2m'))
- self.assertFalse(
- 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.'
- )
- through_key = ('missing', 'Through')
- self.assertFalse(
- any(lookup[0:2] == fk_lookup for lookup in opts.apps._pending_lookups.get(through_key, [])),
- 'Pending lookup added for the abstract model many-to-many `through` parameter.'
+ pending_ops_before = list(apps._pending_operations.items())
+
+ class AbstractManyToManyModel(models.Model):
+ fk = models.ForeignKey('missing.FK')
+
+ class Meta:
+ abstract = True
+
+ self.assertIs(AbstractManyToManyModel._meta.apps, apps)
+ self.assertEqual(
+ pending_ops_before,
+ list(apps._pending_operations.items()),
+ "Pending lookup added for a many-to-many field on an abstract model"
)