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.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.fields.related import ManyToManyField
from django.db.models.loading import BaseAppCache
class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
@ -38,20 +38,19 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
for field in delete_fields: for field in delete_fields:
del body[field.name] del body[field.name]
del mapping[field.column] del mapping[field.column]
# Work inside a new AppCache
app_cache = BaseAppCache()
# Construct a new model for the new state # Construct a new model for the new state
meta_contents = { meta_contents = {
'app_label': model._meta.app_label, 'app_label': model._meta.app_label,
'db_table': model._meta.db_table + "__new", 'db_table': model._meta.db_table + "__new",
'unique_together': model._meta.unique_together if override_uniques is None else override_uniques, 'unique_together': model._meta.unique_together if override_uniques is None else override_uniques,
'app_cache': app_cache,
} }
meta = type("Meta", tuple(), meta_contents) meta = type("Meta", tuple(), meta_contents)
body['Meta'] = meta body['Meta'] = meta
body['__module__'] = model.__module__ 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) temp_model = type(model._meta.object_name, model.__bases__, body)
cache.set_cache(default_cache)
# Create a new table with that format # Create a new table with that format
self.create_model(temp_model) self.create_model(temp_model)
# Copy data from the old table # 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) return self._alter_many_to_many(model, old_field, new_field, strict)
elif old_type is None or new_type is None: 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)" % ( 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, old_field,
new_field, new_field,
)) ))
# Alter by remaking table # Alter by remaking table
self._remake_table(model, alter_fields=[(old_field, new_field)]) 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.deletion import Collector
from django.db.models.options import Options from django.db.models.options import Options
from django.db.models import signals 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.translation import ugettext_lazy as _
from django.utils.functional import curry from django.utils.functional import curry
from django.utils.encoding import force_str, force_text 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) new_class._base_manager = new_class._base_manager._copy_to_model(new_class)
# Bail out early if we have already created this 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) seed_cache=False, only_installed=False)
if m is not None: if m is not None:
return m return m
@ -242,16 +241,13 @@ class ModelBase(type):
new_class._prepare() new_class._prepare()
if new_class._meta.auto_register: new_class._meta.app_cache.register_models(new_class._meta.app_label, new_class)
register_models(new_class._meta.app_label, new_class) # Because of the way imports happen (recursively), we may or may not be
# 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
# 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
# should only be one class for each model, so we always return the # registered version.
# registered version. return new_class._meta.app_cache.get_model(new_class._meta.app_label, name,
return get_model(new_class._meta.app_label, name, seed_cache=False, only_installed=False)
seed_cache=False, only_installed=False)
else:
return new_class
def copy_managers(cls, base_managers): def copy_managers(cls, base_managers):
# This is in-place sorting of an Options attribute, but that's fine. # 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') '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 A cache that stores installed applications and their models. Used to
provide reverse-relations and for app introspection (e.g. admin). 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): def __init__(self):
# Keys of app_store are the model modules for each application. self.__dict__ = _initialize()
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 = {}
def _populate(self): def _populate(self):
""" """
Fill in all the cache information. This method is threadsafe, in the Stub method - this base class does no auto-loading.
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: 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): def _label_for(self, app_mod):
""" """
@ -253,42 +248,58 @@ class AppCache(object):
self.register_models(app_label, *models.values()) 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 A cache that stores installed applications and their models. Used to
imported references to 'cache' are changed along with it. provide reverse-relations and for app introspection (e.g. admin).
Borg version of the BaseAppCache class.
""" """
def __init__(self, cache): __shared_state = _initialize()
self._cache = cache
def set_cache(self, cache): def __init__(self):
self._cache = cache self.__dict__ = self.__shared_state
def __getattr__(self, attr): def _populate(self):
if attr in ("_cache", "set_cache"): """
return self.__dict__[attr] Fill in all the cache information. This method is threadsafe, in the
return getattr(self._cache, attr) sense that every caller will see the same state upon return, and if the
cache is already initialised, it does no work.
def __setattr__(self, attr, value): """
if attr in ("_cache", "set_cache"): if self.loaded:
self.__dict__[attr] = value
return 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()
cache = AppCache()
default_cache = AppCache()
cache = AppCacheWrapper(default_cache)
# These methods were always module level, so are kept that way for backwards # These methods were always module level, so are kept that way for backwards
# compatibility. These are wrapped with lambdas to stop the attribute # compatibility.
# access resolving directly to a method on a single cache instance. get_apps = cache.get_apps
get_apps = lambda *x, **y: cache.get_apps(*x, **y) get_app = cache.get_app
get_app = lambda *x, **y: cache.get_app(*x, **y) get_app_errors = cache.get_app_errors
get_app_errors = lambda *x, **y: cache.get_app_errors(*x, **y) get_models = cache.get_models
get_models = lambda *x, **y: cache.get_models(*x, **y) get_model = cache.get_model
get_model = lambda *x, **y: cache.get_model(*x, **y) register_models = cache.register_models
register_models = lambda *x, **y: cache.register_models(*x, **y) load_app = cache.load_app
load_app = lambda *x, **y: cache.load_app(*x, **y) app_cache_ready = cache.app_cache_ready
app_cache_ready = lambda *x, **y: cache.app_cache_ready(*x, **y)

View File

@ -8,7 +8,7 @@ from django.conf import settings
from django.db.models.fields.related import ManyToManyRel from django.db.models.fields.related import ManyToManyRel
from django.db.models.fields import AutoField, FieldDoesNotExist from django.db.models.fields import AutoField, FieldDoesNotExist
from django.db.models.fields.proxy import OrderWrt 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 import six
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.datastructures import SortedDict 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', DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
'unique_together', 'permissions', 'get_latest_by', 'unique_together', 'permissions', 'get_latest_by',
'order_with_respect_to', 'app_label', 'db_tablespace', '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 @python_2_unicode_compatible
@ -70,8 +70,8 @@ class Options(object):
# from *other* models. Needed for some admin checks. Internal use only. # from *other* models. Needed for some admin checks. Internal use only.
self.related_fkey_lookups = [] self.related_fkey_lookups = []
# If we should auto-register with the AppCache # A custom AppCache to use, if you're making a separate model set.
self.auto_register = True self.app_cache = cache
def contribute_to_class(self, cls, name): def contribute_to_class(self, cls, name):
from django.db import connection from django.db import connection

View File

@ -1,8 +1,11 @@
from django.db import models 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, # 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 # these models are all inserted into a separate AppCache so the main test
# a schema test is running. # runner doesn't syncdb them.
new_app_cache = BaseAppCache()
class Author(models.Model): class Author(models.Model):
@ -10,24 +13,24 @@ class Author(models.Model):
height = models.PositiveIntegerField(null=True, blank=True) height = models.PositiveIntegerField(null=True, blank=True)
class Meta: class Meta:
auto_register = False app_cache = new_app_cache
class AuthorWithM2M(models.Model): class AuthorWithM2M(models.Model):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
class Meta: class Meta:
auto_register = False app_cache = new_app_cache
class Book(models.Model): class Book(models.Model):
author = models.ForeignKey(Author) author = models.ForeignKey(Author)
title = models.CharField(max_length=100, db_index=True) title = models.CharField(max_length=100, db_index=True)
pub_date = models.DateTimeField() pub_date = models.DateTimeField()
#tags = models.ManyToManyField("Tag", related_name="books") # tags = models.ManyToManyField("Tag", related_name="books")
class Meta: class Meta:
auto_register = False app_cache = new_app_cache
class BookWithM2M(models.Model): class BookWithM2M(models.Model):
@ -37,7 +40,7 @@ class BookWithM2M(models.Model):
tags = models.ManyToManyField("Tag", related_name="books") tags = models.ManyToManyField("Tag", related_name="books")
class Meta: class Meta:
auto_register = False app_cache = new_app_cache
class BookWithSlug(models.Model): class BookWithSlug(models.Model):
@ -47,7 +50,7 @@ class BookWithSlug(models.Model):
slug = models.CharField(max_length=20, unique=True) slug = models.CharField(max_length=20, unique=True)
class Meta: class Meta:
auto_register = False app_cache = new_app_cache
db_table = "schema_book" db_table = "schema_book"
@ -56,7 +59,7 @@ class Tag(models.Model):
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
class Meta: class Meta:
auto_register = False app_cache = new_app_cache
class TagUniqueRename(models.Model): class TagUniqueRename(models.Model):
@ -64,7 +67,7 @@ class TagUniqueRename(models.Model):
slug2 = models.SlugField(unique=True) slug2 = models.SlugField(unique=True)
class Meta: class Meta:
auto_register = False app_cache = new_app_cache
db_table = "schema_tag" db_table = "schema_tag"
@ -73,5 +76,5 @@ class UniqueTest(models.Model):
slug = models.SlugField(unique=False) slug = models.SlugField(unique=False)
class Meta: class Meta:
auto_register = False app_cache = new_app_cache
unique_together = ["year", "slug"] unique_together = ["year", "slug"]

View File

@ -1,12 +1,10 @@
from __future__ import absolute_import from __future__ import absolute_import
import copy
import datetime import datetime
from django.test import TransactionTestCase from django.test import TransactionTestCase
from django.utils.unittest import skipUnless from django.utils.unittest import skipUnless
from django.db import connection, DatabaseError, IntegrityError from django.db import connection, DatabaseError, IntegrityError
from django.db.models.fields import IntegerField, TextField, CharField, SlugField from django.db.models.fields import IntegerField, TextField, CharField, SlugField
from django.db.models.fields.related import ManyToManyField, ForeignKey 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 from .models import Author, AuthorWithM2M, Book, BookWithSlug, BookWithM2M, Tag, TagUniqueRename, UniqueTest
@ -27,14 +25,6 @@ class SchemaTests(TransactionTestCase):
def setUp(self): def setUp(self):
# Make sure we're in manual transaction mode # Make sure we're in manual transaction mode
connection.set_autocommit(False) 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): def tearDown(self):
# Delete any tables made for our models # Delete any tables made for our models
@ -43,8 +33,6 @@ class SchemaTests(TransactionTestCase):
# Rollback anything that may have happened # Rollback anything that may have happened
connection.rollback() connection.rollback()
connection.set_autocommit(True) 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): def delete_tables(self):
"Deletes all model tables for our models for a clean test environment" "Deletes all model tables for our models for a clean test environment"