mirror of https://github.com/django/django.git
Refs #26421 -- Refactored Apps.lazy_model_operation() for better checks and tests
This commit is contained in:
parent
0eac5535f7
commit
2ff7ef15b0
|
@ -372,29 +372,35 @@ class Apps(object):
|
||||||
The function passed to this method must accept exactly n models as
|
The function passed to this method must accept exactly n models as
|
||||||
arguments, where n=len(model_keys).
|
arguments, where n=len(model_keys).
|
||||||
"""
|
"""
|
||||||
# If this function depends on more than one model, we recursively turn
|
# Base case: no arguments, just execute the function.
|
||||||
# it into a chain of functions that accept a single model argument and
|
if not model_keys:
|
||||||
# pass each in turn to lazy_model_operation.
|
function()
|
||||||
model_key, more_models = model_keys[0], model_keys[1:]
|
# Recursive case: take the head of model_keys, wait for the
|
||||||
if more_models:
|
# corresponding model class to be imported and registered, then apply
|
||||||
supplied_fn = function
|
# that argument to the supplied function. Pass the resulting partial
|
||||||
|
# to lazy_model_operation() along with the remaining model args and
|
||||||
def function(model):
|
# repeat until all models are loaded and all arguments are applied.
|
||||||
next_function = partial(supplied_fn, model)
|
|
||||||
# Annotate the function with its field for retrieval in
|
|
||||||
# migrations.state.StateApps.
|
|
||||||
if getattr(supplied_fn, 'keywords', None):
|
|
||||||
next_function.field = supplied_fn.keywords.get('field')
|
|
||||||
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:
|
else:
|
||||||
function(model_class)
|
next_model, more_models = model_keys[0], model_keys[1:]
|
||||||
|
|
||||||
|
# This will be executed after the class corresponding to next_model
|
||||||
|
# has been imported and registered. The `func` attribute provides
|
||||||
|
# duck-type compatibility with partials.
|
||||||
|
def apply_next_model(model):
|
||||||
|
next_function = partial(apply_next_model.func, model)
|
||||||
|
self.lazy_model_operation(next_function, *more_models)
|
||||||
|
apply_next_model.func = function
|
||||||
|
|
||||||
|
# If the model has already been imported and registered, partially
|
||||||
|
# apply it to the function now. If not, add it to the list of
|
||||||
|
# pending operations for the model, where it will be executed with
|
||||||
|
# the model class as its sole argument once the model is ready.
|
||||||
|
try:
|
||||||
|
model_class = self.get_registered_model(*next_model)
|
||||||
|
except LookupError:
|
||||||
|
self._pending_operations[next_model].append(apply_next_model)
|
||||||
|
else:
|
||||||
|
apply_next_model(model_class)
|
||||||
|
|
||||||
def do_pending_operations(self, model):
|
def do_pending_operations(self, model):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -318,14 +318,23 @@ class GenericRelation(ForeignObject):
|
||||||
errors.extend(self._check_generic_foreign_key_existence())
|
errors.extend(self._check_generic_foreign_key_existence())
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
def _is_matching_generic_foreign_key(self, field):
|
||||||
|
"""
|
||||||
|
Return True if field is a GenericForeignKey whose content type and
|
||||||
|
object id fields correspond to the equivalent attributes on this
|
||||||
|
GenericRelation.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
isinstance(field, GenericForeignKey) and
|
||||||
|
field.ct_field == self.content_type_field_name and
|
||||||
|
field.fk_field == self.object_id_field_name
|
||||||
|
)
|
||||||
|
|
||||||
def _check_generic_foreign_key_existence(self):
|
def _check_generic_foreign_key_existence(self):
|
||||||
target = self.remote_field.model
|
target = self.remote_field.model
|
||||||
if isinstance(target, ModelBase):
|
if isinstance(target, ModelBase):
|
||||||
fields = target._meta.private_fields
|
fields = target._meta.private_fields
|
||||||
if any(isinstance(field, GenericForeignKey) and
|
if any(self._is_matching_generic_foreign_key(field) for field in fields):
|
||||||
field.ct_field == self.content_type_field_name and
|
|
||||||
field.fk_field == self.object_id_field_name
|
|
||||||
for field in fields):
|
|
||||||
return []
|
return []
|
||||||
else:
|
else:
|
||||||
return [
|
return [
|
||||||
|
@ -401,20 +410,16 @@ class GenericRelation(ForeignObject):
|
||||||
self.model = cls
|
self.model = cls
|
||||||
setattr(cls, self.name, ReverseGenericManyToOneDescriptor(self.remote_field))
|
setattr(cls, self.name, ReverseGenericManyToOneDescriptor(self.remote_field))
|
||||||
|
|
||||||
# Add get_RELATED_order() and set_RELATED_order() methods if the model
|
# Add get_RELATED_order() and set_RELATED_order() to the model this
|
||||||
# on the other end of this relation is ordered with respect to this.
|
# field belongs to, if the model on the other end of this relation
|
||||||
def matching_gfk(field):
|
# is ordered with respect to its corresponding GenericForeignKey.
|
||||||
return (
|
if not cls._meta.abstract:
|
||||||
isinstance(field, GenericForeignKey) and
|
|
||||||
self.content_type_field_name == field.ct_field and
|
|
||||||
self.object_id_field_name == field.fk_field
|
|
||||||
)
|
|
||||||
|
|
||||||
def make_generic_foreign_order_accessors(related_model, model):
|
def make_generic_foreign_order_accessors(related_model, model):
|
||||||
if matching_gfk(model._meta.order_with_respect_to):
|
if self._is_matching_generic_foreign_key(model._meta.order_with_respect_to):
|
||||||
make_foreign_order_accessors(model, related_model)
|
make_foreign_order_accessors(model, related_model)
|
||||||
|
|
||||||
lazy_related_operation(make_generic_foreign_order_accessors, self.model, self.remote_field.model)
|
lazy_related_operation(make_generic_foreign_order_accessors, self.model, self.remote_field.model)
|
||||||
|
|
||||||
def set_attributes_from_rel(self):
|
def set_attributes_from_rel(self):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -63,3 +63,99 @@ def check_model_signals(app_configs=None, **kwargs):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def _check_lazy_references(apps, ignore=None):
|
||||||
|
"""
|
||||||
|
Ensure all lazy (i.e. string) model references have been resolved.
|
||||||
|
|
||||||
|
Lazy references are used in various places throughout Django, primarily in
|
||||||
|
related fields and model signals. Identify those common cases and provide
|
||||||
|
more helpful error messages for them.
|
||||||
|
|
||||||
|
The ignore parameter is used by StateApps to exclude swappable models from
|
||||||
|
this check.
|
||||||
|
"""
|
||||||
|
pending_models = set(apps._pending_operations) - (ignore or set())
|
||||||
|
|
||||||
|
# Short circuit if there aren't any errors.
|
||||||
|
if not pending_models:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def extract_operation(obj):
|
||||||
|
"""
|
||||||
|
Take a callable found in Apps._pending_operations and identify the
|
||||||
|
original callable passed to Apps.lazy_model_operation(). If that
|
||||||
|
callable was a partial, return the inner, non-partial function and
|
||||||
|
any arguments and keyword arguments that were supplied with it.
|
||||||
|
|
||||||
|
obj is a callback defined locally in Apps.lazy_model_operation() and
|
||||||
|
annotated there with a `func` attribute so as to imitate a partial.
|
||||||
|
"""
|
||||||
|
operation, args, keywords = obj, [], {}
|
||||||
|
while hasattr(operation, 'func'):
|
||||||
|
# The or clauses are redundant but work around a bug (#25945) in
|
||||||
|
# functools.partial in Python 3 <= 3.5.1 and Python 2 <= 2.7.11.
|
||||||
|
args.extend(getattr(operation, 'args', []) or [])
|
||||||
|
keywords.update(getattr(operation, 'keywords', {}) or {})
|
||||||
|
operation = operation.func
|
||||||
|
return operation, args, keywords
|
||||||
|
|
||||||
|
def app_model_error(model_key):
|
||||||
|
try:
|
||||||
|
apps.get_app_config(model_key[0])
|
||||||
|
model_error = "app '%s' doesn't provide model '%s'" % model_key
|
||||||
|
except LookupError:
|
||||||
|
model_error = "app '%s' isn't installed" % model_key[0]
|
||||||
|
return model_error
|
||||||
|
|
||||||
|
# Here are several functions which return CheckMessage instances for the
|
||||||
|
# most common usages of lazy operations throughout Django. These functions
|
||||||
|
# take the model that was being waited on as an (app_label, modelname)
|
||||||
|
# pair, the original lazy function, and its positional and keyword args as
|
||||||
|
# determined by extract_operation().
|
||||||
|
|
||||||
|
def field_error(model_key, func, args, keywords):
|
||||||
|
error_msg = (
|
||||||
|
"The field %(field)s was declared with a lazy reference "
|
||||||
|
"to '%(model)s', but %(model_error)s."
|
||||||
|
)
|
||||||
|
params = {
|
||||||
|
'model': '.'.join(model_key),
|
||||||
|
'field': keywords['field'],
|
||||||
|
'model_error': app_model_error(model_key),
|
||||||
|
}
|
||||||
|
return Error(error_msg % params, obj=keywords['field'], id='fields.E307')
|
||||||
|
|
||||||
|
def default_error(model_key, func, args, keywords):
|
||||||
|
error_msg = "%(op)s contains a lazy reference to %(model)s, but %(model_error)s."
|
||||||
|
params = {
|
||||||
|
'op': func,
|
||||||
|
'model': '.'.join(model_key),
|
||||||
|
'model_error': app_model_error(model_key),
|
||||||
|
}
|
||||||
|
return Error(error_msg % params, obj=func, id='models.E022')
|
||||||
|
|
||||||
|
# Maps common uses of lazy operations to corresponding error functions
|
||||||
|
# defined above. If a key maps to None, no error will be produced.
|
||||||
|
# default_error() will be used for usages that don't appear in this dict.
|
||||||
|
known_lazy = {
|
||||||
|
('django.db.models.fields.related', 'resolve_related_class'): field_error,
|
||||||
|
('django.db.models.fields.related', 'set_managed'): None,
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_error(model_key, func, args, keywords):
|
||||||
|
key = (func.__module__, func.__name__)
|
||||||
|
error_fn = known_lazy.get(key, default_error)
|
||||||
|
return error_fn(model_key, func, args, keywords) if error_fn else None
|
||||||
|
|
||||||
|
return sorted(filter(None, (
|
||||||
|
build_error(model_key, *extract_operation(func))
|
||||||
|
for model_key in pending_models
|
||||||
|
for func in apps._pending_operations[model_key]
|
||||||
|
)), key=lambda error: error.msg)
|
||||||
|
|
||||||
|
|
||||||
|
@register(Tags.models)
|
||||||
|
def check_lazy_references(app_configs=None, **kwargs):
|
||||||
|
return _check_lazy_references(apps)
|
||||||
|
|
|
@ -240,44 +240,11 @@ class StateApps(Apps):
|
||||||
self.render_multiple(list(models.values()) + self.real_models)
|
self.render_multiple(list(models.values()) + self.real_models)
|
||||||
|
|
||||||
# There shouldn't be any operations pending at this point.
|
# There shouldn't be any operations pending at this point.
|
||||||
pending_models = set(self._pending_operations)
|
from django.core.checks.model_checks import _check_lazy_references
|
||||||
if ignore_swappable:
|
ignore = {make_model_tuple(settings.AUTH_USER_MODEL)} if ignore_swappable else set()
|
||||||
pending_models -= {make_model_tuple(settings.AUTH_USER_MODEL)}
|
errors = _check_lazy_references(self, ignore=ignore)
|
||||||
if pending_models:
|
if errors:
|
||||||
raise ValueError(self._pending_models_error(pending_models))
|
raise ValueError("\n".join(error.msg for error in errors))
|
||||||
|
|
||||||
def _pending_models_error(self, pending_models):
|
|
||||||
"""
|
|
||||||
Almost all internal uses of lazy operations are to resolve string model
|
|
||||||
references in related fields. We can extract the fields from those
|
|
||||||
operations and use them to provide a nicer error message.
|
|
||||||
|
|
||||||
This will work for any function passed to lazy_related_operation() that
|
|
||||||
has a keyword argument called 'field'.
|
|
||||||
"""
|
|
||||||
def extract_field(operation):
|
|
||||||
# operation is annotated with the field in
|
|
||||||
# apps.registry.Apps.lazy_model_operation().
|
|
||||||
return getattr(operation, 'field', None)
|
|
||||||
|
|
||||||
def extract_field_names(operations):
|
|
||||||
return (str(field) for field in map(extract_field, operations) if field)
|
|
||||||
|
|
||||||
get_ops = self._pending_operations.__getitem__
|
|
||||||
# Ordered list of pairs of the form
|
|
||||||
# ((app_label, model_name), [field_name_1, field_name_2, ...])
|
|
||||||
models_fields = sorted(
|
|
||||||
(model_key, sorted(extract_field_names(get_ops(model_key))))
|
|
||||||
for model_key in pending_models
|
|
||||||
)
|
|
||||||
|
|
||||||
def model_text(model_key, fields):
|
|
||||||
field_list = ", ".join(fields)
|
|
||||||
field_text = " (referred to by fields: %s)" % field_list if fields else ""
|
|
||||||
return ("%s.%s" % model_key) + field_text
|
|
||||||
|
|
||||||
msg = "Unhandled pending operations for models:"
|
|
||||||
return "\n ".join([msg] + [model_text(*i) for i in models_fields])
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def bulk_update(self):
|
def bulk_update(self):
|
||||||
|
|
|
@ -7,12 +7,18 @@ def make_model_tuple(model):
|
||||||
corresponding ("app_label", "modelname") tuple. If a tuple is passed in,
|
corresponding ("app_label", "modelname") tuple. If a tuple is passed in,
|
||||||
it's assumed to be a valid model tuple already and returned unchanged.
|
it's assumed to be a valid model tuple already and returned unchanged.
|
||||||
"""
|
"""
|
||||||
if isinstance(model, tuple):
|
try:
|
||||||
model_tuple = model
|
if isinstance(model, tuple):
|
||||||
elif isinstance(model, six.string_types):
|
model_tuple = model
|
||||||
app_label, model_name = model.split(".")
|
elif isinstance(model, six.string_types):
|
||||||
model_tuple = app_label, model_name.lower()
|
app_label, model_name = model.split(".")
|
||||||
else:
|
model_tuple = app_label, model_name.lower()
|
||||||
model_tuple = model._meta.app_label, model._meta.model_name
|
else:
|
||||||
assert len(model_tuple) == 2, "Invalid model representation: %s" % model
|
model_tuple = model._meta.app_label, model._meta.model_name
|
||||||
return model_tuple
|
assert len(model_tuple) == 2
|
||||||
|
return model_tuple
|
||||||
|
except (ValueError, AssertionError):
|
||||||
|
raise ValueError(
|
||||||
|
"Invalid model reference '%s'. String model references "
|
||||||
|
"must be of the form 'app_label.ModelName'." % model
|
||||||
|
)
|
||||||
|
|
|
@ -138,6 +138,9 @@ Models
|
||||||
* **models.E020**: The ``<model>.check()`` class method is currently overridden.
|
* **models.E020**: The ``<model>.check()`` class method is currently overridden.
|
||||||
* **models.E021**: ``ordering`` and ``order_with_respect_to`` cannot be used
|
* **models.E021**: ``ordering`` and ``order_with_respect_to`` cannot be used
|
||||||
together.
|
together.
|
||||||
|
* **models.E022**: ``<function>`` contains a lazy reference to
|
||||||
|
``<app label>>.<model>``, but app ``<app label>`` isn't installed or
|
||||||
|
doesn't provide model ``<model>``.
|
||||||
|
|
||||||
Fields
|
Fields
|
||||||
~~~~~~
|
~~~~~~
|
||||||
|
@ -200,6 +203,9 @@ Related Fields
|
||||||
for ``<field name>``.
|
for ``<field name>``.
|
||||||
* **fields.E306**: Related name must be a valid Python identifier or end with
|
* **fields.E306**: Related name must be a valid Python identifier or end with
|
||||||
a ``'+'``.
|
a ``'+'``.
|
||||||
|
* **fields.E307**: The field ``<app label>.<model>.<field name>`` was declared
|
||||||
|
with a lazy reference to ``<app label>.<model>``, but app ``<app label>``
|
||||||
|
isn't installed or doesn't provide model ``<model>``.
|
||||||
* **fields.E310**: No subset of the fields ``<field1>``, ``<field2>``, ... on
|
* **fields.E310**: No subset of the fields ``<field1>``, ``<field2>``, ... on
|
||||||
model ``<model>`` is unique. Add ``unique=True`` on any of those fields or
|
model ``<model>`` is unique. Add ``unique=True`` on any of those fields or
|
||||||
add at least a subset of them to a unique_together constraint.
|
add at least a subset of them to a unique_together constraint.
|
||||||
|
|
|
@ -449,6 +449,12 @@ class MigrateTests(MigrationTestBase):
|
||||||
"""
|
"""
|
||||||
call_command("migrate", "migrated_unapplied_app", stdout=six.StringIO())
|
call_command("migrate", "migrated_unapplied_app", stdout=six.StringIO())
|
||||||
|
|
||||||
|
# unmigrated_app.SillyModel has a foreign key to 'migrations.Tribble',
|
||||||
|
# but that model is only defined in a migration, so the global app
|
||||||
|
# registry never sees it and the reference is left dangling. Remove it
|
||||||
|
# to avoid problems in subsequent tests.
|
||||||
|
del apps._pending_operations[('migrations', 'tribble')]
|
||||||
|
|
||||||
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_squashed"})
|
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_squashed"})
|
||||||
def test_migrate_record_replaced(self):
|
def test_migrate_record_replaced(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -661,9 +661,10 @@ class StateTests(SimpleTestCase):
|
||||||
project_state = ProjectState()
|
project_state = ProjectState()
|
||||||
project_state.add_model(ModelState.from_model(Book))
|
project_state.add_model(ModelState.from_model(Book))
|
||||||
msg = (
|
msg = (
|
||||||
"Unhandled pending operations for models:\n"
|
"The field migrations.Book.author was declared with a lazy reference "
|
||||||
" migrations.author (referred to by fields: migrations.Book.author)\n"
|
"to 'migrations.author', but app 'migrations' doesn't provide model 'author'.\n"
|
||||||
" migrations.publisher (referred to by fields: migrations.Book.publisher)"
|
"The field migrations.Book.publisher was declared with a lazy reference "
|
||||||
|
"to 'migrations.publisher', but app 'migrations' doesn't provide model 'publisher'."
|
||||||
)
|
)
|
||||||
with self.assertRaisesMessage(ValueError, msg):
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
project_state.apps
|
project_state.apps
|
||||||
|
@ -672,9 +673,10 @@ class StateTests(SimpleTestCase):
|
||||||
project_state = ProjectState()
|
project_state = ProjectState()
|
||||||
project_state.add_model(ModelState.from_model(Magazine))
|
project_state.add_model(ModelState.from_model(Magazine))
|
||||||
msg = (
|
msg = (
|
||||||
"Unhandled pending operations for models:\n"
|
"The field migrations.Magazine.authors was declared with a lazy reference "
|
||||||
" migrations.author (referred to by fields: "
|
"to 'migrations.author\', but app 'migrations' doesn't provide model 'author'.\n"
|
||||||
"migrations.Magazine.authors, migrations.Magazine_authors.author)"
|
"The field migrations.Magazine_authors.author was declared with a lazy reference "
|
||||||
|
"to \'migrations.author\', but app 'migrations' doesn't provide model 'author'."
|
||||||
)
|
)
|
||||||
with self.assertRaisesMessage(ValueError, msg):
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
project_state.apps
|
project_state.apps
|
||||||
|
@ -682,10 +684,14 @@ class StateTests(SimpleTestCase):
|
||||||
# And now with multiple models and multiple fields.
|
# And now with multiple models and multiple fields.
|
||||||
project_state.add_model(ModelState.from_model(Book))
|
project_state.add_model(ModelState.from_model(Book))
|
||||||
msg = (
|
msg = (
|
||||||
"Unhandled pending operations for models:\n"
|
"The field migrations.Book.author was declared with a lazy reference "
|
||||||
" migrations.author (referred to by fields: migrations.Book.author, "
|
"to 'migrations.author', but app 'migrations' doesn't provide model 'author'.\n"
|
||||||
"migrations.Magazine.authors, migrations.Magazine_authors.author)\n"
|
"The field migrations.Book.publisher was declared with a lazy reference "
|
||||||
" migrations.publisher (referred to by fields: migrations.Book.publisher)"
|
"to 'migrations.publisher', but app 'migrations' doesn't provide model 'publisher'.\n"
|
||||||
|
"The field migrations.Magazine.authors was declared with a lazy reference "
|
||||||
|
"to 'migrations.author', but app 'migrations' doesn't provide model 'author'.\n"
|
||||||
|
"The field migrations.Magazine_authors.author was declared with a lazy reference "
|
||||||
|
"to 'migrations.author', but app 'migrations' doesn't provide model 'author'."
|
||||||
)
|
)
|
||||||
with self.assertRaisesMessage(ValueError, msg):
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
project_state.apps
|
project_state.apps
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
from django.core import management
|
from django.core import management
|
||||||
from django.core.checks import Error, run_checks
|
from django.core.checks import Error, run_checks
|
||||||
|
from django.core.checks.model_checks import _check_lazy_references
|
||||||
|
from django.db import models
|
||||||
from django.db.models.signals import post_init
|
from django.db.models.signals import post_init
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import isolate_apps, override_settings
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,6 +17,10 @@ def on_post_init(**kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def dummy_function(model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
INSTALLED_APPS=['django.contrib.auth', 'django.contrib.contenttypes'],
|
INSTALLED_APPS=['django.contrib.auth', 'django.contrib.contenttypes'],
|
||||||
SILENCED_SYSTEM_CHECKS=['fields.W342'], # ForeignKey(unique=True)
|
SILENCED_SYSTEM_CHECKS=['fields.W342'], # ForeignKey(unique=True)
|
||||||
|
@ -54,3 +60,42 @@ class ModelValidationTest(SimpleTestCase):
|
||||||
self.assertEqual(errors, expected)
|
self.assertEqual(errors, expected)
|
||||||
|
|
||||||
post_init.unresolved_references = unresolved_references
|
post_init.unresolved_references = unresolved_references
|
||||||
|
|
||||||
|
@isolate_apps('django.contrib.auth', kwarg_name='apps')
|
||||||
|
def test_lazy_reference_checks(self, apps):
|
||||||
|
|
||||||
|
class DummyModel(models.Model):
|
||||||
|
author = models.ForeignKey('Author', models.CASCADE)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = "model_validation"
|
||||||
|
|
||||||
|
apps.lazy_model_operation(dummy_function, ('auth', 'imaginarymodel'))
|
||||||
|
apps.lazy_model_operation(dummy_function, ('fanciful_app', 'imaginarymodel'))
|
||||||
|
|
||||||
|
errors = _check_lazy_references(apps)
|
||||||
|
|
||||||
|
expected = [
|
||||||
|
Error(
|
||||||
|
"%r contains a lazy reference to auth.imaginarymodel, "
|
||||||
|
"but app 'auth' doesn't provide model 'imaginarymodel'." % dummy_function,
|
||||||
|
obj=dummy_function,
|
||||||
|
id='models.E022',
|
||||||
|
),
|
||||||
|
Error(
|
||||||
|
"%r contains a lazy reference to fanciful_app.imaginarymodel, "
|
||||||
|
"but app 'fanciful_app' isn't installed." % dummy_function,
|
||||||
|
obj=dummy_function,
|
||||||
|
id='models.E022',
|
||||||
|
),
|
||||||
|
Error(
|
||||||
|
"The field model_validation.DummyModel.author was declared "
|
||||||
|
"with a lazy reference to 'model_validation.author', but app "
|
||||||
|
"'model_validation' isn't installed.",
|
||||||
|
hint=None,
|
||||||
|
obj=DummyModel.author.field,
|
||||||
|
id='fields.E307',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(errors, expected)
|
||||||
|
|
Loading…
Reference in New Issue