Split out a BaseAppCache, make AppCache borg again, add _meta.app_cache

This commit is contained in:
Andrew Godwin 2013-05-09 15:16:43 +01:00
parent 941d23e548
commit 104ad0504b
6 changed files with 114 additions and 117 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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