From 104ad0504b4b123277b3f0e7c0be7fb9e84c2d72 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Thu, 9 May 2013 15:16:43 +0100 Subject: [PATCH] Split out a BaseAppCache, make AppCache borg again, add _meta.app_cache --- django/db/backends/sqlite3/schema.py | 15 ++- django/db/models/base.py | 20 ++-- django/db/models/loading.py | 151 ++++++++++++++------------- django/db/models/options.py | 8 +- tests/schema/models.py | 25 +++-- tests/schema/tests.py | 12 --- 6 files changed, 114 insertions(+), 117 deletions(-) diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py index c1df0c79817..de32dfd8936 100644 --- a/django/db/backends/sqlite3/schema.py +++ b/django/db/backends/sqlite3/schema.py @@ -1,6 +1,6 @@ from django.db.backends.schema import BaseDatabaseSchemaEditor -from django.db.models.loading import cache, default_cache, AppCache from django.db.models.fields.related import ManyToManyField +from django.db.models.loading import BaseAppCache class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): @@ -38,20 +38,19 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): for field in delete_fields: del body[field.name] del mapping[field.column] + # Work inside a new AppCache + app_cache = BaseAppCache() # Construct a new model for the new state meta_contents = { 'app_label': model._meta.app_label, 'db_table': model._meta.db_table + "__new", 'unique_together': model._meta.unique_together if override_uniques is None else override_uniques, + 'app_cache': app_cache, } meta = type("Meta", tuple(), meta_contents) body['Meta'] = meta body['__module__'] = model.__module__ - self.app_cache = AppCache() - cache.set_cache(self.app_cache) - cache.copy_from(default_cache) temp_model = type(model._meta.object_name, model.__bases__, body) - cache.set_cache(default_cache) # Create a new table with that format self.create_model(temp_model) # Copy data from the old table @@ -117,9 +116,9 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): return self._alter_many_to_many(model, old_field, new_field, strict) elif old_type is None or new_type is None: raise ValueError("Cannot alter field %s into %s - they are not compatible types (probably means only one is an M2M with implicit through model)" % ( - old_field, - new_field, - )) + old_field, + new_field, + )) # Alter by remaking table self._remake_table(model, alter_fields=[(old_field, new_field)]) diff --git a/django/db/models/base.py b/django/db/models/base.py index 8a87a63ffe0..d6870b561ae 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -19,7 +19,6 @@ from django.db.models.query_utils import DeferredAttribute, deferred_class_facto from django.db.models.deletion import Collector from django.db.models.options import Options from django.db.models import signals -from django.db.models.loading import register_models, get_model from django.utils.translation import ugettext_lazy as _ from django.utils.functional import curry from django.utils.encoding import force_str, force_text @@ -134,7 +133,7 @@ class ModelBase(type): new_class._base_manager = new_class._base_manager._copy_to_model(new_class) # Bail out early if we have already created this class. - m = get_model(new_class._meta.app_label, name, + m = new_class._meta.app_cache.get_model(new_class._meta.app_label, name, seed_cache=False, only_installed=False) if m is not None: return m @@ -242,16 +241,13 @@ class ModelBase(type): new_class._prepare() - if new_class._meta.auto_register: - register_models(new_class._meta.app_label, new_class) - # Because of the way imports happen (recursively), we may or may not be - # the first time this model tries to register with the framework. There - # should only be one class for each model, so we always return the - # registered version. - return get_model(new_class._meta.app_label, name, - seed_cache=False, only_installed=False) - else: - return new_class + new_class._meta.app_cache.register_models(new_class._meta.app_label, new_class) + # Because of the way imports happen (recursively), we may or may not be + # the first time this model tries to register with the framework. There + # should only be one class for each model, so we always return the + # registered version. + return new_class._meta.app_cache.get_model(new_class._meta.app_label, name, + seed_cache=False, only_installed=False) def copy_managers(cls, base_managers): # This is in-place sorting of an Options attribute, but that's fine. diff --git a/django/db/models/loading.py b/django/db/models/loading.py index 412bd76e0da..61273e512a8 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -16,57 +16,52 @@ __all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models', 'load_app', 'app_cache_ready') -class AppCache(object): +def _initialize(): + """ + Returns a dictionary to be used as the initial value of the + [shared] state of the app cache. + """ + return dict( + # Keys of app_store are the model modules for each application. + app_store = SortedDict(), + + # Mapping of installed app_labels to model modules for that app. + app_labels = {}, + + # Mapping of app_labels to a dictionary of model names to model code. + # May contain apps that are not installed. + app_models = SortedDict(), + + # Mapping of app_labels to errors raised when trying to import the app. + app_errors = {}, + + # -- Everything below here is only used when populating the cache -- + loaded = False, + handled = {}, + postponed = [], + nesting_level = 0, + _get_models_cache = {}, + ) + + +class BaseAppCache(object): """ A cache that stores installed applications and their models. Used to provide reverse-relations and for app introspection (e.g. admin). + + This provides the base (non-Borg) AppCache class - the AppCache + subclass adds borg-like behaviour for the few cases where it's needed, + and adds the code that auto-loads from INSTALLED_APPS. """ def __init__(self): - # Keys of app_store are the model modules for each application. - self.app_store = SortedDict() - # Mapping of installed app_labels to model modules for that app. - self.app_labels = {} - # Mapping of app_labels to a dictionary of model names to model code. - # May contain apps that are not installed. - self.app_models = SortedDict() - # Mapping of app_labels to errors raised when trying to import the app. - self.app_errors = {} - # -- Everything below here is only used when populating the cache -- - self.loaded = False - self.handled = {} - self.postponed = [] - self.nesting_level = 0 - self._get_models_cache = {} + self.__dict__ = _initialize() 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. + Stub method - this base class does no auto-loading. """ - 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() + self.loaded = True def _label_for(self, app_mod): """ @@ -253,42 +248,58 @@ class AppCache(object): self.register_models(app_label, *models.values()) -class AppCacheWrapper(object): +class AppCache(BaseAppCache): """ - As AppCache can be changed at runtime, this class wraps it so any - imported references to 'cache' are changed along with it. + A cache that stores installed applications and their models. Used to + provide reverse-relations and for app introspection (e.g. admin). + + Borg version of the BaseAppCache class. """ - def __init__(self, cache): - self._cache = cache + __shared_state = _initialize() - def set_cache(self, cache): - self._cache = cache + def __init__(self): + self.__dict__ = self.__shared_state - def __getattr__(self, attr): - if attr in ("_cache", "set_cache"): - return self.__dict__[attr] - return getattr(self._cache, attr) - - def __setattr__(self, attr, value): - if attr in ("_cache", "set_cache"): - self.__dict__[attr] = value + 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 - return setattr(self._cache, attr, value) + # 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() - -default_cache = AppCache() -cache = AppCacheWrapper(default_cache) +cache = AppCache() # These methods were always module level, so are kept that way for backwards -# compatibility. These are wrapped with lambdas to stop the attribute -# access resolving directly to a method on a single cache instance. -get_apps = lambda *x, **y: cache.get_apps(*x, **y) -get_app = lambda *x, **y: cache.get_app(*x, **y) -get_app_errors = lambda *x, **y: cache.get_app_errors(*x, **y) -get_models = lambda *x, **y: cache.get_models(*x, **y) -get_model = lambda *x, **y: cache.get_model(*x, **y) -register_models = lambda *x, **y: cache.register_models(*x, **y) -load_app = lambda *x, **y: cache.load_app(*x, **y) -app_cache_ready = lambda *x, **y: cache.app_cache_ready(*x, **y) +# compatibility. +get_apps = cache.get_apps +get_app = cache.get_app +get_app_errors = cache.get_app_errors +get_models = cache.get_models +get_model = cache.get_model +register_models = cache.register_models +load_app = cache.load_app +app_cache_ready = cache.app_cache_ready diff --git a/django/db/models/options.py b/django/db/models/options.py index a878fe28c4d..7ca2f1c321e 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -8,7 +8,7 @@ from django.conf import settings from django.db.models.fields.related import ManyToManyRel from django.db.models.fields import AutoField, FieldDoesNotExist from django.db.models.fields.proxy import OrderWrt -from django.db.models.loading import get_models, app_cache_ready +from django.db.models.loading import get_models, app_cache_ready, cache from django.utils import six from django.utils.functional import cached_property from django.utils.datastructures import SortedDict @@ -21,7 +21,7 @@ get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]| DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering', 'unique_together', 'permissions', 'get_latest_by', 'order_with_respect_to', 'app_label', 'db_tablespace', - 'abstract', 'managed', 'proxy', 'swappable', 'auto_created', 'index_together', 'auto_register') + 'abstract', 'managed', 'proxy', 'swappable', 'auto_created', 'index_together', 'app_cache') @python_2_unicode_compatible @@ -70,8 +70,8 @@ class Options(object): # from *other* models. Needed for some admin checks. Internal use only. self.related_fkey_lookups = [] - # If we should auto-register with the AppCache - self.auto_register = True + # A custom AppCache to use, if you're making a separate model set. + self.app_cache = cache def contribute_to_class(self, cls, name): from django.db import connection diff --git a/tests/schema/models.py b/tests/schema/models.py index fdf950860cb..a160b9aaa8f 100644 --- a/tests/schema/models.py +++ b/tests/schema/models.py @@ -1,8 +1,11 @@ from django.db import models +from django.db.models.loading import BaseAppCache # Because we want to test creation and deletion of these as separate things, -# these models are all marked as unmanaged and only marked as managed while -# a schema test is running. +# these models are all inserted into a separate AppCache so the main test +# runner doesn't syncdb them. + +new_app_cache = BaseAppCache() class Author(models.Model): @@ -10,24 +13,24 @@ class Author(models.Model): height = models.PositiveIntegerField(null=True, blank=True) class Meta: - auto_register = False + app_cache = new_app_cache class AuthorWithM2M(models.Model): name = models.CharField(max_length=255) class Meta: - auto_register = False + app_cache = new_app_cache class Book(models.Model): author = models.ForeignKey(Author) title = models.CharField(max_length=100, db_index=True) pub_date = models.DateTimeField() - #tags = models.ManyToManyField("Tag", related_name="books") + # tags = models.ManyToManyField("Tag", related_name="books") class Meta: - auto_register = False + app_cache = new_app_cache class BookWithM2M(models.Model): @@ -37,7 +40,7 @@ class BookWithM2M(models.Model): tags = models.ManyToManyField("Tag", related_name="books") class Meta: - auto_register = False + app_cache = new_app_cache class BookWithSlug(models.Model): @@ -47,7 +50,7 @@ class BookWithSlug(models.Model): slug = models.CharField(max_length=20, unique=True) class Meta: - auto_register = False + app_cache = new_app_cache db_table = "schema_book" @@ -56,7 +59,7 @@ class Tag(models.Model): slug = models.SlugField(unique=True) class Meta: - auto_register = False + app_cache = new_app_cache class TagUniqueRename(models.Model): @@ -64,7 +67,7 @@ class TagUniqueRename(models.Model): slug2 = models.SlugField(unique=True) class Meta: - auto_register = False + app_cache = new_app_cache db_table = "schema_tag" @@ -73,5 +76,5 @@ class UniqueTest(models.Model): slug = models.SlugField(unique=False) class Meta: - auto_register = False + app_cache = new_app_cache unique_together = ["year", "slug"] diff --git a/tests/schema/tests.py b/tests/schema/tests.py index bd4ae0db342..85e1dfc9ea5 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -1,12 +1,10 @@ from __future__ import absolute_import -import copy 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, default_cache, AppCache from .models import Author, AuthorWithM2M, Book, BookWithSlug, BookWithM2M, Tag, TagUniqueRename, UniqueTest @@ -27,14 +25,6 @@ class SchemaTests(TransactionTestCase): def setUp(self): # Make sure we're in manual transaction mode connection.set_autocommit(False) - # The unmanaged models need to be removed after the test in order to - # prevent bad interactions with the flush operation in other tests. - self.app_cache = AppCache() - cache.set_cache(self.app_cache) - cache.copy_from(default_cache) - for model in self.models: - cache.register_models("schema", model) - model._prepare() def tearDown(self): # Delete any tables made for our models @@ -43,8 +33,6 @@ class SchemaTests(TransactionTestCase): # Rollback anything that may have happened connection.rollback() connection.set_autocommit(True) - cache.set_cache(default_cache) - cache.app_models['schema'] = {} # One M2M gets left in the old cache def delete_tables(self): "Deletes all model tables for our models for a clean test environment"