Remove AppCache state handling, replace with swappable caches

This commit is contained in:
Andrew Godwin 2012-09-22 00:47:04 +01:00
parent dbc17d035b
commit 49d1e6b0e2
6 changed files with 72 additions and 83 deletions

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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"]

View File

@ -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"