diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 7fc7a092fd708..c30ba03489c5c 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -2,7 +2,7 @@ from django.db import connection, connections, router from django.db.backends import util -from django.db.models import signals, get_model +from django.db.models import signals from django.db.models.fields import (AutoField, Field, IntegerField, PositiveIntegerField, PositiveSmallIntegerField, FieldDoesNotExist) from django.db.models.related import RelatedObject, PathInfo @@ -18,8 +18,6 @@ RECURSIVE_RELATIONSHIP_CONSTANT = 'self' -pending_lookups = {} - def add_lazy_relation(cls, field, relation, operation): """ @@ -70,14 +68,14 @@ class MyModel(Model): # string right away. If get_model returns None, it means that the related # model isn't loaded yet, so we need to pend the relation until the class # is prepared. - model = get_model(app_label, model_name, + model = cls._meta.app_cache.get_model(app_label, model_name, seed_cache=False, only_installed=False) if model: operation(field, model, cls) else: key = (app_label, model_name) value = (cls, field, operation) - pending_lookups.setdefault(key, []).append(value) + cls._meta.app_cache.pending_lookups.setdefault(key, []).append(value) def do_pending_lookups(sender, **kwargs): @@ -85,7 +83,7 @@ def do_pending_lookups(sender, **kwargs): Handle any pending relations to the sending model. Sent from class_prepared. """ key = (sender._meta.app_label, sender.__name__) - for cls, field, operation in pending_lookups.pop(key, []): + for cls, field, operation in sender._meta.app_cache.pending_lookups.pop(key, []): operation(field, sender, cls) signals.class_prepared.connect(do_pending_lookups) @@ -1330,6 +1328,7 @@ def set_managed(field, model, cls): 'unique_together': (from_, to), 'verbose_name': '%(from)s-%(to)s relationship' % {'from': from_, 'to': to}, 'verbose_name_plural': '%(from)s-%(to)s relationships' % {'from': from_, 'to': to}, + 'app_cache': field.model._meta.app_cache, }) # Construct and return the new class. return type(str(name), (models.Model,), { diff --git a/django/db/models/loading.py b/django/db/models/loading.py index 61273e512a87a..075cae4c61b55 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -35,7 +35,11 @@ def _initialize(): # Mapping of app_labels to errors raised when trying to import the app. app_errors = {}, + # Pending lookups for lazy relations + pending_lookups = {}, + # -- Everything below here is only used when populating the cache -- + loads_installed = True, loaded = False, handled = {}, postponed = [], @@ -56,12 +60,44 @@ class BaseAppCache(object): def __init__(self): self.__dict__ = _initialize() + # This stops _populate loading from INSTALLED_APPS and ignores the + # only_installed arguments to get_model[s] + self.loads_installed = False def _populate(self): """ Stub method - this base class does no auto-loading. """ - self.loaded = True + """ + Fill in all the cache information. This method is threadsafe, in the + sense that every caller will see the same state upon return, and if the + cache is already initialised, it does no work. + """ + if self.loaded: + return + if not self.loads_installed: + self.loaded = True + return + # Note that we want to use the import lock here - the app loading is + # in many cases initiated implicitly by importing, and thus it is + # possible to end up in deadlock when one thread initiates loading + # without holding the importer lock and another thread then tries to + # import something which also launches the app loading. For details of + # this situation see #18251. + imp.acquire_lock() + try: + if self.loaded: + return + for app_name in settings.INSTALLED_APPS: + if app_name in self.handled: + continue + self.load_app(app_name, True) + if not self.nesting_level: + for app_name in self.postponed: + self.load_app(app_name) + self.loaded = True + finally: + imp.release_lock() def _label_for(self, app_mod): """ @@ -169,12 +205,15 @@ def get_models(self, app_mod=None, By default, models that aren't part of installed apps will *not* be included in the list of models. However, if you specify - only_installed=False, they will be. + only_installed=False, they will be. If you're using a non-default + AppCache, this argument does nothing - all models will be included. By default, models that have been swapped out will *not* be included in the list of models. However, if you specify include_swapped, they will be. """ + if not self.loads_installed: + only_installed = False cache_key = (app_mod, include_auto_created, include_deferred, only_installed, include_swapped) try: return self._get_models_cache[cache_key] @@ -212,6 +251,8 @@ def get_model(self, app_label, model_name, Returns None if no model is found. """ + if not self.loads_installed: + only_installed = False if seed_cache: self._populate() if only_installed and app_label not in self.app_labels: @@ -241,12 +282,6 @@ def register_models(self, app_label, *models): model_dict[model_name] = model self._get_models_cache.clear() - def copy_from(self, other): - "Registers all models from the other cache into this one" - cache._populate() - for app_label, models in other.app_models.items(): - self.register_models(app_label, *models.values()) - class AppCache(BaseAppCache): """ @@ -261,35 +296,6 @@ class AppCache(BaseAppCache): def __init__(self): self.__dict__ = self.__shared_state - def _populate(self): - """ - Fill in all the cache information. This method is threadsafe, in the - sense that every caller will see the same state upon return, and if the - cache is already initialised, it does no work. - """ - if self.loaded: - return - # Note that we want to use the import lock here - the app loading is - # in many cases initiated implicitly by importing, and thus it is - # possible to end up in deadlock when one thread initiates loading - # without holding the importer lock and another thread then tries to - # import something which also launches the app loading. For details of - # this situation see #18251. - imp.acquire_lock() - try: - if self.loaded: - return - for app_name in settings.INSTALLED_APPS: - if app_name in self.handled: - continue - self.load_app(app_name, True) - if not self.nesting_level: - for app_name in self.postponed: - self.load_app(app_name) - self.loaded = True - finally: - imp.release_lock() - cache = AppCache() diff --git a/tests/app_cache/__init__.py b/tests/app_cache/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/app_cache/models.py b/tests/app_cache/models.py new file mode 100644 index 0000000000000..1b4d33c2f9928 --- /dev/null +++ b/tests/app_cache/models.py @@ -0,0 +1,17 @@ +from django.db import models +from django.db.models.loading import BaseAppCache + +# We're testing app cache presence on load, so this is handy. + +new_app_cache = BaseAppCache() + + +class TotallyNormal(models.Model): + name = models.CharField(max_length=255) + + +class SoAlternative(models.Model): + name = models.CharField(max_length=255) + + class Meta: + app_cache = new_app_cache diff --git a/tests/app_cache/tests.py b/tests/app_cache/tests.py new file mode 100644 index 0000000000000..42598d90c7338 --- /dev/null +++ b/tests/app_cache/tests.py @@ -0,0 +1,50 @@ +from __future__ import absolute_import +import datetime +from django.test import TransactionTestCase +from django.utils.unittest import skipUnless +from django.db import connection, DatabaseError, IntegrityError +from django.db.models.fields import IntegerField, TextField, CharField, SlugField +from django.db.models.fields.related import ManyToManyField, ForeignKey +from django.db.models.loading import cache, BaseAppCache +from django.db import models +from .models import TotallyNormal, SoAlternative, new_app_cache + + +class AppCacheTests(TransactionTestCase): + """ + Tests the AppCache borg and non-borg versions + """ + + def test_models_py(self): + """ + Tests that the models in the models.py file were loaded correctly. + """ + + self.assertEqual(cache.get_model("app_cache", "TotallyNormal"), TotallyNormal) + self.assertEqual(cache.get_model("app_cache", "SoAlternative"), None) + + self.assertEqual(new_app_cache.get_model("app_cache", "TotallyNormal"), None) + self.assertEqual(new_app_cache.get_model("app_cache", "SoAlternative"), SoAlternative) + + def test_dynamic_load(self): + """ + Makes a new model at runtime and ensures it goes into the right place. + """ + old_models = cache.get_models(cache.get_app("app_cache")) + # Construct a new model in a new app cache + body = {} + new_app_cache = BaseAppCache() + meta_contents = { + 'app_label': "app_cache", + 'app_cache': new_app_cache, + } + meta = type("Meta", tuple(), meta_contents) + body['Meta'] = meta + body['__module__'] = TotallyNormal.__module__ + temp_model = type("SouthPonies", (models.Model,), body) + # Make sure it appeared in the right place! + self.assertEqual( + old_models, + cache.get_models(cache.get_app("app_cache")), + ) + self.assertEqual(new_app_cache.get_model("app_cache", "SouthPonies"), temp_model)