diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py index e42f2c62d9..a1fd95fe8b 100644 --- a/django/db/backends/sqlite3/schema.py +++ b/django/db/backends/sqlite3/schema.py @@ -1,5 +1,5 @@ from django.db.backends.schema import BaseDatabaseSchemaEditor -from django.db.models.loading import cache +from django.db.models.loading import cache, default_cache, AppCache from django.db.models.fields.related import ManyToManyField @@ -46,10 +46,12 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): } meta = type("Meta", tuple(), meta_contents) body['Meta'] = meta - body['__module__'] = "__fake__" - with cache.temporary_state(): - del cache.app_models[model._meta.app_label][model._meta.object_name.lower()] - temp_model = type(model._meta.object_name, model.__bases__, body) + 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 diff --git a/django/db/models/base.py b/django/db/models/base.py index 62024c8ee4..de94911601 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -228,14 +228,17 @@ class ModelBase(type): return new_class new_class._prepare() - 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) + + 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 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 d57780c578..6ae37505a5 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -236,69 +236,49 @@ class AppCache(object): model_dict[model_name] = model self._get_models_cache.clear() - def save_state(self): - """ - Returns an object that contains the current AppCache state. - Can be provided to restore_state to undo actions. - """ - return { - "app_store": SortedDict(self.app_store.items()), - "app_labels": dict(self.app_labels.items()), - "app_models": SortedDict((k, SortedDict(v.items())) for k, v in self.app_models.items()), - "app_errors": dict(self.app_errors.items()), - } - - def restore_state(self, state): - """ - Restores the AppCache to a previous state from save_state. - Note that the state is used by reference, not copied in. - """ - self.app_store = state['app_store'] - self.app_labels = state['app_labels'] - self.app_models = state['app_models'] - self.app_errors = state['app_errors'] - self._get_models_cache.clear() - - def temporary_state(self): - "Returns a context manager that restores the state on exit" - return StateContextManager(self) - - def unregister_all(self): - """ - Wipes the AppCache clean of all registered models. - Used for things like migration libraries' fake ORMs. - """ - self.app_store = SortedDict() - self.app_labels = {} - self.app_models = SortedDict() - self.app_errors = {} + 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 StateContextManager(object): +class AppCacheWrapper(object): """ - Context manager for locking cache state. - Useful for making temporary models you don't want to stay in the cache. + As AppCache can be changed at runtime, this class wraps it so any + imported references to 'cache' are changed along with it. """ def __init__(self, cache): - self.cache = cache + self._cache = cache - def __enter__(self): - self.state = self.cache.save_state() + def set_cache(self, cache): + self._cache = cache - def __exit__(self, type, value, traceback): - self.cache.restore_state(self.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 + return + return setattr(self._cache, attr, value) -cache = AppCache() +default_cache = AppCache() +cache = AppCacheWrapper(default_cache) + # These methods were always module level, so are kept that way for backwards -# 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 +# 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) diff --git a/django/db/models/options.py b/django/db/models/options.py index 6814ce27ff..820540559f 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -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', 'auto_created') + 'abstract', 'managed', 'proxy', 'auto_created', 'auto_register') @python_2_unicode_compatible class Options(object): @@ -68,6 +68,9 @@ 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 + def contribute_to_class(self, cls, name): from django.db import connection from django.db.backends.util import truncate_name diff --git a/tests/modeltests/schema/models.py b/tests/modeltests/schema/models.py index f3d6d09e2e..fdf950860c 100644 --- a/tests/modeltests/schema/models.py +++ b/tests/modeltests/schema/models.py @@ -10,14 +10,14 @@ class Author(models.Model): height = models.PositiveIntegerField(null=True, blank=True) class Meta: - managed = False + auto_register = False class AuthorWithM2M(models.Model): name = models.CharField(max_length=255) class Meta: - managed = False + auto_register = False class Book(models.Model): @@ -27,7 +27,7 @@ class Book(models.Model): #tags = models.ManyToManyField("Tag", related_name="books") class Meta: - managed = False + auto_register = False class BookWithM2M(models.Model): @@ -37,7 +37,7 @@ class BookWithM2M(models.Model): tags = models.ManyToManyField("Tag", related_name="books") class Meta: - managed = False + auto_register = False class BookWithSlug(models.Model): @@ -47,7 +47,7 @@ class BookWithSlug(models.Model): slug = models.CharField(max_length=20, unique=True) class Meta: - managed = False + auto_register = False db_table = "schema_book" @@ -56,7 +56,7 @@ class Tag(models.Model): slug = models.SlugField(unique=True) class Meta: - managed = False + auto_register = False class TagUniqueRename(models.Model): @@ -64,7 +64,7 @@ class TagUniqueRename(models.Model): slug2 = models.SlugField(unique=True) class Meta: - managed = False + auto_register = False db_table = "schema_tag" @@ -73,5 +73,5 @@ class UniqueTest(models.Model): slug = models.SlugField(unique=False) class Meta: - managed = False + auto_register = False unique_together = ["year", "slug"] diff --git a/tests/modeltests/schema/tests.py b/tests/modeltests/schema/tests.py index 7c3e6ffc03..5fabe6e91d 100644 --- a/tests/modeltests/schema/tests.py +++ b/tests/modeltests/schema/tests.py @@ -6,7 +6,7 @@ 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 +from django.db.models.loading import cache, default_cache, AppCache from .models import Author, AuthorWithM2M, Book, BookWithSlug, BookWithM2M, Tag, TagUniqueRename, UniqueTest @@ -30,9 +30,12 @@ class SchemaTests(TestCase): connection.managed(True) # The unmanaged models need to be removed after the test in order to # prevent bad interactions with the flush operation in other tests. - self.cache_state = cache.save_state() + self.app_cache = AppCache() + cache.set_cache(self.app_cache) + cache.copy_from(default_cache) for model in self.models: - model._meta.managed = True + cache.register_models("schema", model) + model._prepare() def tearDown(self): # Delete any tables made for our models @@ -40,10 +43,8 @@ class SchemaTests(TestCase): # Rollback anything that may have happened connection.rollback() connection.leave_transaction_management() - # Unhook our models - for model in self.models: - model._meta.managed = False - cache.restore_state(self.cache_state) + 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"