Fixed #32233 -- Cleaned-up duplicate connection functionality.

This commit is contained in:
Florian Apolloner 2017-10-22 17:30:42 +02:00 committed by Mariusz Felisiak
parent 0f00560d45
commit 98e05ccde4
9 changed files with 143 additions and 147 deletions

View File

@ -12,13 +12,11 @@ object.
See docs/topics/cache.txt for information on the public API.
"""
from asgiref.local import Local
from django.conf import settings
from django.core import signals
from django.core.cache.backends.base import (
BaseCache, CacheKeyWarning, InvalidCacheBackendError, InvalidCacheKey,
)
from django.utils.connection import BaseConnectionHandler, ConnectionProxy
from django.utils.module_loading import import_string
__all__ = [
@ -29,28 +27,12 @@ __all__ = [
DEFAULT_CACHE_ALIAS = 'default'
class CacheHandler:
"""
A Cache Handler to manage access to Cache instances.
class CacheHandler(BaseConnectionHandler):
settings_name = 'CACHES'
exception_class = InvalidCacheBackendError
Ensure only one instance of each alias exists per thread.
"""
def __init__(self):
self._caches = Local()
def __getitem__(self, alias):
try:
return self._caches.caches[alias]
except AttributeError:
self._caches.caches = {}
except KeyError:
pass
if alias not in settings.CACHES:
raise InvalidCacheBackendError(
"Could not find config for '%s' in settings.CACHES" % alias
)
params = settings.CACHES[alias].copy()
def create_connection(self, alias):
params = self.settings[alias].copy()
backend = params.pop('BACKEND')
location = params.pop('LOCATION', '')
try:
@ -58,42 +40,13 @@ class CacheHandler:
except ImportError as e:
raise InvalidCacheBackendError(
"Could not find backend '%s': %s" % (backend, e)
)
cache = backend_cls(location, params)
self._caches.caches[alias] = cache
return cache
def all(self):
return getattr(self._caches, 'caches', {}).values()
) from e
return backend_cls(location, params)
caches = CacheHandler()
class DefaultCacheProxy:
"""
Proxy access to the default Cache object's attributes.
This allows the legacy `cache` object to be thread-safe using the new
``caches`` API.
"""
def __getattr__(self, name):
return getattr(caches[DEFAULT_CACHE_ALIAS], name)
def __setattr__(self, name, value):
return setattr(caches[DEFAULT_CACHE_ALIAS], name, value)
def __delattr__(self, name):
return delattr(caches[DEFAULT_CACHE_ALIAS], name)
def __contains__(self, key):
return key in caches[DEFAULT_CACHE_ALIAS]
def __eq__(self, other):
return caches[DEFAULT_CACHE_ALIAS] == other
cache = DefaultCacheProxy()
cache = ConnectionProxy(caches, DEFAULT_CACHE_ALIAS)
def close_caches(**kwargs):

View File

@ -5,6 +5,7 @@ from django.db.utils import (
InterfaceError, InternalError, NotSupportedError, OperationalError,
ProgrammingError,
)
from django.utils.connection import ConnectionProxy
__all__ = [
'connection', 'connections', 'router', 'DatabaseError', 'IntegrityError',
@ -17,28 +18,8 @@ connections = ConnectionHandler()
router = ConnectionRouter()
class DefaultConnectionProxy:
"""
Proxy for accessing the default DatabaseWrapper object's attributes. If you
need to access the DatabaseWrapper object itself, use
connections[DEFAULT_DB_ALIAS] instead.
"""
def __getattr__(self, item):
return getattr(connections[DEFAULT_DB_ALIAS], item)
def __setattr__(self, name, value):
return setattr(connections[DEFAULT_DB_ALIAS], name, value)
def __delattr__(self, name):
return delattr(connections[DEFAULT_DB_ALIAS], name)
def __eq__(self, other):
return connections[DEFAULT_DB_ALIAS] == other
# For backwards compatibility. Prefer connections['default'] instead.
connection = DefaultConnectionProxy()
connection = ConnectionProxy(connections, DEFAULT_DB_ALIAS)
# Register an event to reset saved queries when a Django request is started.

View File

@ -2,10 +2,11 @@ import pkgutil
from importlib import import_module
from pathlib import Path
from asgiref.local import Local
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
# For backwards compatibility with Django < 3.2
from django.utils.connection import ConnectionDoesNotExist # NOQA: F401
from django.utils.connection import BaseConnectionHandler
from django.utils.functional import cached_property
from django.utils.module_loading import import_string
@ -131,39 +132,30 @@ def load_backend(backend_name):
raise
class ConnectionDoesNotExist(Exception):
pass
class ConnectionHandler(BaseConnectionHandler):
settings_name = 'DATABASES'
# Connections needs to still be an actual thread local, as it's truly
# thread-critical. Database backends should use @async_unsafe to protect
# their code from async contexts, but this will give those contexts
# separate connections in case it's needed as well. There's no cleanup
# after async contexts, though, so we don't allow that if we can help it.
thread_critical = True
def configure_settings(self, databases):
databases = super().configure_settings(databases)
if databases == {}:
databases[DEFAULT_DB_ALIAS] = {'ENGINE': 'django.db.backends.dummy'}
elif DEFAULT_DB_ALIAS not in databases:
raise ImproperlyConfigured(
f"You must define a '{DEFAULT_DB_ALIAS}' database."
)
elif databases[DEFAULT_DB_ALIAS] == {}:
databases[DEFAULT_DB_ALIAS]['ENGINE'] = 'django.db.backends.dummy'
return databases
class ConnectionHandler:
def __init__(self, databases=None):
"""
databases is an optional dictionary of database definitions (structured
like settings.DATABASES).
"""
self._databases = databases
# Connections needs to still be an actual thread local, as it's truly
# thread-critical. Database backends should use @async_unsafe to protect
# their code from async contexts, but this will give those contexts
# separate connections in case it's needed as well. There's no cleanup
# after async contexts, though, so we don't allow that if we can help it.
self._connections = Local(thread_critical=True)
@cached_property
@property
def databases(self):
if self._databases is None:
self._databases = settings.DATABASES
if self._databases == {}:
self._databases = {
DEFAULT_DB_ALIAS: {
'ENGINE': 'django.db.backends.dummy',
},
}
if DEFAULT_DB_ALIAS not in self._databases:
raise ImproperlyConfigured("You must define a '%s' database." % DEFAULT_DB_ALIAS)
if self._databases[DEFAULT_DB_ALIAS] == {}:
self._databases[DEFAULT_DB_ALIAS]['ENGINE'] = 'django.db.backends.dummy'
return self._databases
return self.settings
def ensure_defaults(self, alias):
"""
@ -173,7 +165,7 @@ class ConnectionHandler:
try:
conn = self.databases[alias]
except KeyError:
raise ConnectionDoesNotExist("The connection %s doesn't exist" % alias)
raise self.exception_class(f"The connection '{alias}' doesn't exist.")
conn.setdefault('ATOMIC_REQUESTS', False)
conn.setdefault('AUTOCOMMIT', True)
@ -193,7 +185,7 @@ class ConnectionHandler:
try:
conn = self.databases[alias]
except KeyError:
raise ConnectionDoesNotExist("The connection %s doesn't exist" % alias)
raise self.exception_class(f"The connection '{alias}' doesn't exist.")
test_settings = conn.setdefault('TEST', {})
default_test_settings = [
@ -206,29 +198,12 @@ class ConnectionHandler:
for key, value in default_test_settings:
test_settings.setdefault(key, value)
def __getitem__(self, alias):
if hasattr(self._connections, alias):
return getattr(self._connections, alias)
def create_connection(self, alias):
self.ensure_defaults(alias)
self.prepare_test_settings(alias)
db = self.databases[alias]
backend = load_backend(db['ENGINE'])
conn = backend.DatabaseWrapper(db, alias)
setattr(self._connections, alias, conn)
return conn
def __setitem__(self, key, value):
setattr(self._connections, key, value)
def __delitem__(self, key):
delattr(self._connections, key)
def __iter__(self):
return iter(self.databases)
def all(self):
return [self[alias] for alias in self]
return backend.DatabaseWrapper(db, alias)
def close_all(self):
for alias in self:

View File

@ -28,7 +28,8 @@ def clear_cache_handlers(**kwargs):
if kwargs['setting'] == 'CACHES':
from django.core.cache import caches, close_caches
close_caches()
caches._caches = Local()
caches._settings = caches.settings = caches.configure_settings(None)
caches._connections = Local()
@receiver(setting_changed)

View File

@ -0,0 +1,76 @@
from asgiref.local import Local
from django.conf import settings as django_settings
from django.utils.functional import cached_property
class ConnectionProxy:
"""Proxy for accessing a connection object's attributes."""
def __init__(self, connections, alias):
self.__dict__['_connections'] = connections
self.__dict__['_alias'] = alias
def __getattr__(self, item):
return getattr(self._connections[self._alias], item)
def __setattr__(self, name, value):
return setattr(self._connections[self._alias], name, value)
def __delattr__(self, name):
return delattr(self._connections[self._alias], name)
def __contains__(self, key):
return key in self._connections[self._alias]
def __eq__(self, other):
return self._connections[self._alias] == other
class ConnectionDoesNotExist(Exception):
pass
class BaseConnectionHandler:
settings_name = None
exception_class = ConnectionDoesNotExist
thread_critical = False
def __init__(self, settings=None):
self._settings = settings
self._connections = Local(self.thread_critical)
@cached_property
def settings(self):
self._settings = self.configure_settings(self._settings)
return self._settings
def configure_settings(self, settings):
if settings is None:
settings = getattr(django_settings, self.settings_name)
return settings
def create_connection(self, alias):
raise NotImplementedError('Subclasses must implement create_connection().')
def __getitem__(self, alias):
try:
return getattr(self._connections, alias)
except AttributeError:
if alias not in self.settings:
raise self.exception_class(f"The connection '{alias}' doesn't exist.")
conn = self.create_connection(alias)
setattr(self._connections, alias, conn)
return conn
def __setitem__(self, key, value):
setattr(self._connections, key, value)
def __delitem__(self, key):
delattr(self._connections, key)
def __iter__(self):
return iter(self.settings)
def all(self):
return [self[alias] for alias in self]

View File

@ -74,7 +74,7 @@ example ``settings.py`` snippet defining two non-default databases, with the
If you attempt to access a database that you haven't defined in your
:setting:`DATABASES` setting, Django will raise a
``django.db.utils.ConnectionDoesNotExist`` exception.
``django.utils.connection.ConnectionDoesNotExist`` exception.
.. _synchronizing_multiple_databases:

19
tests/cache/tests.py vendored
View File

@ -17,7 +17,8 @@ from unittest import mock, skipIf
from django.conf import settings
from django.core import management, signals
from django.core.cache import (
DEFAULT_CACHE_ALIAS, CacheKeyWarning, InvalidCacheKey, cache, caches,
DEFAULT_CACHE_ALIAS, CacheHandler, CacheKeyWarning, InvalidCacheKey, cache,
caches,
)
from django.core.cache.backends.base import InvalidCacheBackendError
from django.core.cache.utils import make_template_fragment_key
@ -2501,19 +2502,19 @@ class CacheHandlerTest(SimpleTestCase):
self.assertIsNot(c[0], c[1])
def test_nonexistent_alias(self):
msg = "Could not find config for 'nonexistent' in settings.CACHES"
msg = "The connection 'nonexistent' doesn't exist."
with self.assertRaisesMessage(InvalidCacheBackendError, msg):
caches['nonexistent']
def test_nonexistent_backend(self):
test_caches = CacheHandler({
'invalid_backend': {
'BACKEND': 'django.nonexistent.NonexistentBackend',
},
})
msg = (
"Could not find backend 'django.nonexistent.NonexistentBackend': "
"No module named 'django.nonexistent'"
)
with self.settings(CACHES={
'invalid_backend': {
'BACKEND': 'django.nonexistent.NonexistentBackend',
},
}):
with self.assertRaisesMessage(InvalidCacheBackendError, msg):
caches['invalid_backend']
with self.assertRaisesMessage(InvalidCacheBackendError, msg):
test_caches['invalid_backend']

View File

@ -3,10 +3,9 @@ import unittest
from django.core.exceptions import ImproperlyConfigured
from django.db import DEFAULT_DB_ALIAS, ProgrammingError, connection
from django.db.utils import (
ConnectionDoesNotExist, ConnectionHandler, load_backend,
)
from django.db.utils import ConnectionHandler, load_backend
from django.test import SimpleTestCase, TestCase
from django.utils.connection import ConnectionDoesNotExist
class ConnectionHandlerTests(SimpleTestCase):
@ -41,7 +40,7 @@ class ConnectionHandlerTests(SimpleTestCase):
conns['other'].ensure_connection()
def test_nonexistent_alias(self):
msg = "The connection nonexistent doesn't exist"
msg = "The connection 'nonexistent' doesn't exist."
conns = ConnectionHandler({
DEFAULT_DB_ALIAS: {'ENGINE': 'django.db.backends.dummy'},
})
@ -49,7 +48,7 @@ class ConnectionHandlerTests(SimpleTestCase):
conns['nonexistent']
def test_ensure_defaults_nonexistent_alias(self):
msg = "The connection nonexistent doesn't exist"
msg = "The connection 'nonexistent' doesn't exist."
conns = ConnectionHandler({
DEFAULT_DB_ALIAS: {'ENGINE': 'django.db.backends.dummy'},
})
@ -57,7 +56,7 @@ class ConnectionHandlerTests(SimpleTestCase):
conns.ensure_defaults('nonexistent')
def test_prepare_test_settings_nonexistent_alias(self):
msg = "The connection nonexistent doesn't exist"
msg = "The connection 'nonexistent' doesn't exist."
conns = ConnectionHandler({
DEFAULT_DB_ALIAS: {'ENGINE': 'django.db.backends.dummy'},
})

View File

@ -0,0 +1,10 @@
from django.test import SimpleTestCase
from django.utils.connection import BaseConnectionHandler
class BaseConnectionHandlerTests(SimpleTestCase):
def test_create_connection(self):
handler = BaseConnectionHandler()
msg = 'Subclasses must implement create_connection().'
with self.assertRaisesMessage(NotImplementedError, msg):
handler.create_connection(None)