utils.py 11 KB

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