utils.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. from importlib import import_module
  2. import os
  3. import pkgutil
  4. from threading import local
  5. import warnings
  6. from django.conf import settings
  7. from django.core.exceptions import ImproperlyConfigured
  8. from django.utils.deprecation import RemovedInDjango18Warning, RemovedInDjango19Warning
  9. from django.utils.functional import cached_property
  10. from django.utils.module_loading import import_string
  11. from django.utils._os import upath
  12. from django.utils import six
  13. DEFAULT_DB_ALIAS = 'default'
  14. class Error(Exception if six.PY3 else StandardError):
  15. pass
  16. class InterfaceError(Error):
  17. pass
  18. class DatabaseError(Error):
  19. pass
  20. class DataError(DatabaseError):
  21. pass
  22. class OperationalError(DatabaseError):
  23. pass
  24. class IntegrityError(DatabaseError):
  25. pass
  26. class InternalError(DatabaseError):
  27. pass
  28. class ProgrammingError(DatabaseError):
  29. pass
  30. class NotSupportedError(DatabaseError):
  31. pass
  32. class DatabaseErrorWrapper(object):
  33. """
  34. Context manager and decorator that re-throws backend-specific database
  35. exceptions using Django's common wrappers.
  36. """
  37. def __init__(self, wrapper):
  38. """
  39. wrapper is a database wrapper.
  40. It must have a Database attribute defining PEP-249 exceptions.
  41. """
  42. self.wrapper = wrapper
  43. def __enter__(self):
  44. pass
  45. def __exit__(self, exc_type, exc_value, traceback):
  46. if exc_type is None:
  47. return
  48. for dj_exc_type in (
  49. DataError,
  50. OperationalError,
  51. IntegrityError,
  52. InternalError,
  53. ProgrammingError,
  54. NotSupportedError,
  55. DatabaseError,
  56. InterfaceError,
  57. Error,
  58. ):
  59. db_exc_type = getattr(self.wrapper.Database, dj_exc_type.__name__)
  60. if issubclass(exc_type, db_exc_type):
  61. dj_exc_value = dj_exc_type(*exc_value.args)
  62. dj_exc_value.__cause__ = exc_value
  63. # Only set the 'errors_occurred' flag for errors that may make
  64. # the connection unusable.
  65. if dj_exc_type not in (DataError, IntegrityError):
  66. self.wrapper.errors_occurred = True
  67. six.reraise(dj_exc_type, dj_exc_value, traceback)
  68. def __call__(self, func):
  69. # Note that we are intentionally not using @wraps here for performance
  70. # reasons. Refs #21109.
  71. def inner(*args, **kwargs):
  72. with self:
  73. return func(*args, **kwargs)
  74. return inner
  75. def load_backend(backend_name):
  76. # Look for a fully qualified database backend name
  77. try:
  78. return import_module('%s.base' % backend_name)
  79. except ImportError as e_user:
  80. # The database backend wasn't found. Display a helpful error message
  81. # listing all possible (built-in) database backends.
  82. backend_dir = os.path.join(os.path.dirname(upath(__file__)), 'backends')
  83. try:
  84. builtin_backends = [
  85. name for _, name, ispkg in pkgutil.iter_modules([backend_dir])
  86. if ispkg and name != 'dummy']
  87. except EnvironmentError:
  88. builtin_backends = []
  89. if backend_name not in ['django.db.backends.%s' % b for b in
  90. builtin_backends]:
  91. backend_reprs = map(repr, sorted(builtin_backends))
  92. error_msg = ("%r isn't an available database backend.\n"
  93. "Try using 'django.db.backends.XXX', where XXX "
  94. "is one of:\n %s\nError was: %s" %
  95. (backend_name, ", ".join(backend_reprs), e_user))
  96. raise ImproperlyConfigured(error_msg)
  97. else:
  98. # If there's some other error, this must be an error in Django
  99. raise
  100. class ConnectionDoesNotExist(Exception):
  101. pass
  102. class ConnectionHandler(object):
  103. def __init__(self, databases=None):
  104. """
  105. databases is an optional dictionary of database definitions (structured
  106. like settings.DATABASES).
  107. """
  108. self._databases = databases
  109. self._connections = local()
  110. @cached_property
  111. def databases(self):
  112. if self._databases is None:
  113. self._databases = settings.DATABASES
  114. if self._databases == {}:
  115. self._databases = {
  116. DEFAULT_DB_ALIAS: {
  117. 'ENGINE': 'django.db.backends.dummy',
  118. },
  119. }
  120. if DEFAULT_DB_ALIAS not in self._databases:
  121. raise ImproperlyConfigured("You must define a '%s' database" % DEFAULT_DB_ALIAS)
  122. return self._databases
  123. def ensure_defaults(self, alias):
  124. """
  125. Puts the defaults into the settings dictionary for a given connection
  126. where no settings is provided.
  127. """
  128. try:
  129. conn = self.databases[alias]
  130. except KeyError:
  131. raise ConnectionDoesNotExist("The connection %s doesn't exist" % alias)
  132. conn.setdefault('ATOMIC_REQUESTS', False)
  133. if settings.TRANSACTIONS_MANAGED:
  134. warnings.warn(
  135. "TRANSACTIONS_MANAGED is deprecated. Use AUTOCOMMIT instead.",
  136. RemovedInDjango18Warning, stacklevel=2)
  137. conn.setdefault('AUTOCOMMIT', False)
  138. conn.setdefault('AUTOCOMMIT', True)
  139. conn.setdefault('ENGINE', 'django.db.backends.dummy')
  140. if conn['ENGINE'] == 'django.db.backends.' or not conn['ENGINE']:
  141. conn['ENGINE'] = 'django.db.backends.dummy'
  142. conn.setdefault('CONN_MAX_AGE', 0)
  143. conn.setdefault('OPTIONS', {})
  144. conn.setdefault('TIME_ZONE', 'UTC' if settings.USE_TZ else settings.TIME_ZONE)
  145. for setting in ['NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']:
  146. conn.setdefault(setting, '')
  147. TEST_SETTING_RENAMES = {
  148. 'CREATE': 'CREATE_DB',
  149. 'USER_CREATE': 'CREATE_USER',
  150. 'PASSWD': 'PASSWORD',
  151. }
  152. TEST_SETTING_RENAMES_REVERSE = {v: k for k, v in TEST_SETTING_RENAMES.items()}
  153. def prepare_test_settings(self, alias):
  154. """
  155. Makes sure the test settings are available in the 'TEST' sub-dictionary.
  156. """
  157. try:
  158. conn = self.databases[alias]
  159. except KeyError:
  160. raise ConnectionDoesNotExist("The connection %s doesn't exist" % alias)
  161. test_dict_set = 'TEST' in conn
  162. test_settings = conn.setdefault('TEST', {})
  163. old_test_settings = {}
  164. for key, value in six.iteritems(conn):
  165. if key.startswith('TEST_'):
  166. new_key = key[5:]
  167. new_key = self.TEST_SETTING_RENAMES.get(new_key, new_key)
  168. old_test_settings[new_key] = value
  169. if old_test_settings:
  170. if test_dict_set:
  171. if test_settings != old_test_settings:
  172. raise ImproperlyConfigured(
  173. "Connection '%s' has mismatched TEST and TEST_* "
  174. "database settings." % alias)
  175. else:
  176. test_settings.update(old_test_settings)
  177. for key, _ in six.iteritems(old_test_settings):
  178. warnings.warn("In Django 1.9 the %s connection setting will be moved "
  179. "to a %s entry in the TEST setting" %
  180. (self.TEST_SETTING_RENAMES_REVERSE.get(key, key), key),
  181. RemovedInDjango19Warning, stacklevel=2)
  182. for key in list(conn.keys()):
  183. if key.startswith('TEST_'):
  184. del conn[key]
  185. # Check that they didn't just use the old name with 'TEST_' removed
  186. for key, new_key in six.iteritems(self.TEST_SETTING_RENAMES):
  187. if key in test_settings:
  188. warnings.warn("Test setting %s was renamed to %s; specified value (%s) ignored" %
  189. (key, new_key, test_settings[key]), stacklevel=2)
  190. for key in ['CHARSET', 'COLLATION', 'NAME', 'MIRROR']:
  191. test_settings.setdefault(key, None)
  192. def __getitem__(self, alias):
  193. if hasattr(self._connections, alias):
  194. return getattr(self._connections, alias)
  195. self.ensure_defaults(alias)
  196. self.prepare_test_settings(alias)
  197. db = self.databases[alias]
  198. backend = load_backend(db['ENGINE'])
  199. conn = backend.DatabaseWrapper(db, alias)
  200. setattr(self._connections, alias, conn)
  201. return conn
  202. def __setitem__(self, key, value):
  203. setattr(self._connections, key, value)
  204. def __delitem__(self, key):
  205. delattr(self._connections, key)
  206. def __iter__(self):
  207. return iter(self.databases)
  208. def all(self):
  209. return [self[alias] for alias in self]
  210. class ConnectionRouter(object):
  211. def __init__(self, routers=None):
  212. """
  213. If routers is not specified, will default to settings.DATABASE_ROUTERS.
  214. """
  215. self._routers = routers
  216. @cached_property
  217. def routers(self):
  218. if self._routers is None:
  219. self._routers = settings.DATABASE_ROUTERS
  220. routers = []
  221. for r in self._routers:
  222. if isinstance(r, six.string_types):
  223. router = import_string(r)()
  224. else:
  225. router = r
  226. routers.append(router)
  227. return routers
  228. def _router_func(action):
  229. def _route_db(self, model, **hints):
  230. chosen_db = None
  231. for router in self.routers:
  232. try:
  233. method = getattr(router, action)
  234. except AttributeError:
  235. # If the router doesn't have a method, skip to the next one.
  236. pass
  237. else:
  238. chosen_db = method(model, **hints)
  239. if chosen_db:
  240. return chosen_db
  241. try:
  242. return hints['instance']._state.db or DEFAULT_DB_ALIAS
  243. except KeyError:
  244. return DEFAULT_DB_ALIAS
  245. return _route_db
  246. db_for_read = _router_func('db_for_read')
  247. db_for_write = _router_func('db_for_write')
  248. def allow_relation(self, obj1, obj2, **hints):
  249. for router in self.routers:
  250. try:
  251. method = router.allow_relation
  252. except AttributeError:
  253. # If the router doesn't have a method, skip to the next one.
  254. pass
  255. else:
  256. allow = method(obj1, obj2, **hints)
  257. if allow is not None:
  258. return allow
  259. return obj1._state.db == obj2._state.db
  260. def allow_migrate(self, db, model):
  261. for router in self.routers:
  262. try:
  263. try:
  264. method = router.allow_migrate
  265. except AttributeError:
  266. method = router.allow_syncdb
  267. warnings.warn(
  268. 'Router.allow_syncdb has been deprecated and will stop working in Django 1.9. '
  269. 'Rename the method to allow_migrate.',
  270. RemovedInDjango19Warning, stacklevel=2)
  271. except AttributeError:
  272. # If the router doesn't have a method, skip to the next one.
  273. pass
  274. else:
  275. allow = method(db, model)
  276. if allow is not None:
  277. return allow
  278. return True
  279. def get_migratable_models(self, app_config, db, include_auto_created=False):
  280. """
  281. Return app models allowed to be synchronized on provided db.
  282. """
  283. models = app_config.get_models(include_auto_created=include_auto_created)
  284. return [model for model in models if self.allow_migrate(db, model)]