Skip to content

Commit 720ff74

Browse files
AlexHilltimgraham
authored andcommitted
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.
1 parent 0f6f80c commit 720ff74

File tree

10 files changed

+234
-168
lines changed

10 files changed

+234
-168
lines changed

django/apps/registry.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import threading
33
import warnings
44
from collections import Counter, OrderedDict, defaultdict
5+
from functools import partial
56

67
from django.core.exceptions import AppRegistryNotReady, ImproperlyConfigured
78
from django.utils import lru_cache
@@ -45,8 +46,10 @@ def __init__(self, installed_apps=()):
4546
# Lock for thread-safe population.
4647
self._lock = threading.Lock()
4748

48-
# Pending lookups for lazy relations.
49-
self._pending_lookups = {}
49+
# Maps ("app_label", "modelname") tuples to lists of functions to be
50+
# called when the corresponding model is ready. Used by this class's
51+
# `lazy_model_operation()` and `do_pending_operations()` methods.
52+
self._pending_operations = defaultdict(list)
5053

5154
# Populate apps and models, unless it's the master registry.
5255
if installed_apps is not None:
@@ -207,6 +210,7 @@ def register_model(self, app_label, model):
207210
"Conflicting '%s' models in application '%s': %s and %s." %
208211
(model_name, app_label, app_models[model_name], model))
209212
app_models[model_name] = model
213+
self.do_pending_operations(model)
210214
self.clear_cache()
211215

212216
def is_installed(self, app_name):
@@ -332,5 +336,42 @@ def clear_cache(self):
332336
for model in app_config.get_models(include_auto_created=True):
333337
model._meta._expire_cache()
334338

339+
def lazy_model_operation(self, function, *model_keys):
340+
"""
341+
Take a function and a number of ("app_label", "modelname") tuples, and
342+
when all the corresponding models have been imported and registered,
343+
call the function with the model classes as its arguments.
344+
345+
The function passed to this method must accept exactly n models as
346+
arguments, where n=len(model_keys).
347+
"""
348+
# If this function depends on more than one model, we recursively turn
349+
# it into a chain of functions that accept a single model argument and
350+
# pass each in turn to lazy_model_operation.
351+
model_key, more_models = model_keys[0], model_keys[1:]
352+
if more_models:
353+
supplied_fn = function
354+
355+
def function(model):
356+
next_function = partial(supplied_fn, model)
357+
self.lazy_model_operation(next_function, *more_models)
358+
359+
# If the model is already loaded, pass it to the function immediately.
360+
# Otherwise, delay execution until the class is prepared.
361+
try:
362+
model_class = self.get_registered_model(*model_key)
363+
except LookupError:
364+
self._pending_operations[model_key].append(function)
365+
else:
366+
function(model_class)
367+
368+
def do_pending_operations(self, model):
369+
"""
370+
Take a newly-prepared model and pass it to each function waiting for
371+
it. This is called at the very end of `Apps.register_model()`.
372+
"""
373+
key = model._meta.app_label, model._meta.model_name
374+
for function in self._pending_operations.pop(key, []):
375+
function(model)
335376

336377
apps = Apps(installed_apps=None)

django/db/migrations/state.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@
88
from django.conf import settings
99
from django.db import models
1010
from django.db.models.fields.proxy import OrderWrt
11-
from django.db.models.fields.related import (
12-
RECURSIVE_RELATIONSHIP_CONSTANT, do_pending_lookups,
13-
)
11+
from django.db.models.fields.related import RECURSIVE_RELATIONSHIP_CONSTANT
1412
from django.db.models.options import DEFAULT_NAMES, normalize_together
13+
from django.db.models.utils import make_model_tuple
1514
from django.utils import six
1615
from django.utils.encoding import force_text, smart_text
1716
from django.utils.functional import cached_property
@@ -214,22 +213,21 @@ def __init__(self, real_apps, models, ignore_swappable=False):
214213
self.render_multiple(list(models.values()) + self.real_models)
215214

216215
# If there are some lookups left, see if we can first resolve them
217-
# ourselves - sometimes fields are added after class_prepared is sent
218-
for lookup_model, operations in self._pending_lookups.items():
216+
# ourselves - sometimes fields are added after a model is registered
217+
for lookup_model in self._pending_operations:
219218
try:
220-
model = self.get_model(lookup_model[0], lookup_model[1])
219+
model = self.get_model(*lookup_model)
221220
except LookupError:
222-
app_label = "%s.%s" % (lookup_model[0], lookup_model[1])
223-
if app_label == settings.AUTH_USER_MODEL and ignore_swappable:
221+
if lookup_model == make_model_tuple(settings.AUTH_USER_MODEL) and ignore_swappable:
224222
continue
225223
# Raise an error with a best-effort helpful message
226224
# (only for the first issue). Error message should look like:
227225
# "ValueError: Lookup failed for model referenced by
228226
# field migrations.Book.author: migrations.Author"
229-
msg = "Lookup failed for model referenced by field {field}: {model[0]}.{model[1]}"
230-
raise ValueError(msg.format(field=operations[0][1], model=lookup_model))
227+
msg = "Lookup failed for model: {model[0]}.{model[1]}"
228+
raise ValueError(msg.format(model=lookup_model))
231229
else:
232-
do_pending_lookups(model)
230+
self.do_pending_operations(model)
233231

234232
def render_multiple(self, model_states):
235233
# We keep trying to render the models in a loop, ignoring invalid
@@ -277,6 +275,7 @@ def register_model(self, app_label, model):
277275
self.app_configs[app_label] = AppConfigStub(app_label)
278276
self.app_configs[app_label].models = OrderedDict()
279277
self.app_configs[app_label].models[model._meta.model_name] = model
278+
self.do_pending_operations(model)
280279
self.clear_cache()
281280

282281
def unregister_model(self, app_label, model_name):

django/db/models/base.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,16 @@
2121
from django.db.models.deletion import Collector
2222
from django.db.models.fields import AutoField
2323
from django.db.models.fields.related import (
24-
ForeignObjectRel, ManyToOneRel, OneToOneField, add_lazy_relation,
24+
ForeignObjectRel, ManyToOneRel, OneToOneField, lazy_related_operation,
25+
resolve_relation,
2526
)
2627
from django.db.models.manager import ensure_default_manager
2728
from django.db.models.options import Options
2829
from django.db.models.query import Q
2930
from django.db.models.query_utils import (
3031
DeferredAttribute, deferred_class_factory,
3132
)
33+
from django.db.models.utils import make_model_tuple
3234
from django.utils import six
3335
from django.utils.encoding import force_str, force_text
3436
from django.utils.functional import curry
@@ -199,8 +201,8 @@ def __new__(cls, name, bases, attrs):
199201
# Locate OneToOneField instances.
200202
for field in base._meta.local_fields:
201203
if isinstance(field, OneToOneField):
202-
parent_links[field.remote_field.model] = field
203-
204+
related = resolve_relation(new_class, field.remote_field.model)
205+
parent_links[make_model_tuple(related)] = field
204206
# Do the appropriate setup for any model parents.
205207
for base in parents:
206208
original_base = base
@@ -223,8 +225,9 @@ def __new__(cls, name, bases, attrs):
223225
if not base._meta.abstract:
224226
# Concrete classes...
225227
base = base._meta.concrete_model
226-
if base in parent_links:
227-
field = parent_links[base]
228+
base_key = make_model_tuple(base)
229+
if base_key in parent_links:
230+
field = parent_links[base_key]
228231
elif not is_proxy:
229232
attr_name = '%s_ptr' % base._meta.model_name
230233
field = OneToOneField(base, name=attr_name,
@@ -305,7 +308,7 @@ def _prepare(cls):
305308

306309
# defer creating accessors on the foreign class until we are
307310
# certain it has been created
308-
def make_foreign_order_accessors(field, model, cls):
311+
def make_foreign_order_accessors(cls, model, field):
309312
setattr(
310313
field.remote_field.model,
311314
'get_%s_order' % cls.__name__.lower(),
@@ -316,12 +319,8 @@ def make_foreign_order_accessors(field, model, cls):
316319
'set_%s_order' % cls.__name__.lower(),
317320
curry(method_set_order, cls)
318321
)
319-
add_lazy_relation(
320-
cls,
321-
opts.order_with_respect_to,
322-
opts.order_with_respect_to.remote_field.model,
323-
make_foreign_order_accessors
324-
)
322+
wrt = opts.order_with_respect_to
323+
lazy_related_operation(make_foreign_order_accessors, cls, wrt.remote_field.model, field=wrt)
325324

326325
# Give the class a docstring -- its definition.
327326
if cls.__doc__ is None:

0 commit comments

Comments
 (0)