utils.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import pkgutil
  2. from importlib import import_module
  3. from django.conf import settings
  4. from django.core.exceptions import ImproperlyConfigured
  5. # For backwards compatibility with Django < 3.2
  6. from django.utils.connection import ConnectionDoesNotExist # NOQA: F401
  7. from django.utils.connection import BaseConnectionHandler
  8. from django.utils.functional import cached_property
  9. from django.utils.module_loading import import_string
  10. DEFAULT_DB_ALIAS = 'default'
  11. DJANGO_VERSION_PICKLE_KEY = '_django_version'
  12. class Error(Exception):
  13. pass
  14. class InterfaceError(Error):
  15. pass
  16. class DatabaseError(Error):
  17. pass
  18. class DataError(DatabaseError):
  19. pass
  20. class OperationalError(DatabaseError):
  21. pass
  22. class IntegrityError(DatabaseError):
  23. pass
  24. class InternalError(DatabaseError):
  25. pass
  26. class ProgrammingError(DatabaseError):
  27. pass
  28. class NotSupportedError(DatabaseError):
  29. pass
  30. class DatabaseErrorWrapper:
  31. """
  32. Context manager and decorator that reraises backend-specific database
  33. exceptions using Django's common wrappers.
  34. """
  35. def __init__(self, wrapper):
  36. """
  37. wrapper is a database wrapper.
  38. It must have a Database attribute defining PEP-249 exceptions.
  39. """
  40. self.wrapper = wrapper
  41. def __enter__(self):
  42. pass
  43. def __exit__(self, exc_type, exc_value, traceback):
  44. if exc_type is None:
  45. return
  46. for dj_exc_type in (
  47. DataError,
  48. OperationalError,
  49. IntegrityError,
  50. InternalError,
  51. ProgrammingError,
  52. NotSupportedError,
  53. DatabaseError,
  54. InterfaceError,
  55. Error,
  56. ):
  57. db_exc_type = getattr(self.wrapper.Database, dj_exc_type.__name__)
  58. if issubclass(exc_type, db_exc_type):
  59. dj_exc_value = dj_exc_type(*exc_value.args)
  60. # Only set the 'errors_occurred' flag for errors that may make
  61. # the connection unusable.
  62. if dj_exc_type not in (DataError, IntegrityError):
  63. self.wrapper.errors_occurred = True
  64. raise dj_exc_value.with_traceback(traceback) from exc_value
  65. def __call__(self, func):
  66. # Note that we are intentionally not using @wraps here for performance
  67. # reasons. Refs #21109.
  68. def inner(*args, **kwargs):
  69. with self:
  70. return func(*args, **kwargs)
  71. return inner
  72. def load_backend(backend_name):
  73. """
  74. Return a database backend's "base" module given a fully qualified database
  75. backend name, or raise an error if it doesn't exist.
  76. """
  77. # This backend was renamed in Django 1.9.
  78. if backend_name == 'django.db.backends.postgresql_psycopg2':
  79. backend_name = 'django.db.backends.postgresql'
  80. try:
  81. return import_module('%s.base' % backend_name)
  82. except ImportError as e_user:
  83. # The database backend wasn't found. Display a helpful error message
  84. # listing all built-in database backends.
  85. import django.db.backends
  86. builtin_backends = [
  87. name for _, name, ispkg in pkgutil.iter_modules(django.db.backends.__path__)
  88. if ispkg and name not in {'base', 'dummy', 'postgresql_psycopg2'}
  89. ]
  90. if backend_name not in ['django.db.backends.%s' % b for b in builtin_backends]:
  91. backend_reprs = map(repr, sorted(builtin_backends))
  92. raise ImproperlyConfigured(
  93. "%r isn't an available database backend or couldn't be "
  94. "imported. Check the above exception. To use one of the "
  95. "built-in backends, use 'django.db.backends.XXX', where XXX "
  96. "is one of:\n"
  97. " %s" % (backend_name, ", ".join(backend_reprs))
  98. ) from e_user
  99. else:
  100. # If there's some other error, this must be an error in Django
  101. raise
  102. class ConnectionHandler(BaseConnectionHandler):
  103. settings_name = 'DATABASES'
  104. # Connections needs to still be an actual thread local, as it's truly
  105. # thread-critical. Database backends should use @async_unsafe to protect
  106. # their code from async contexts, but this will give those contexts
  107. # separate connections in case it's needed as well. There's no cleanup
  108. # after async contexts, though, so we don't allow that if we can help it.
  109. thread_critical = True
  110. def configure_settings(self, databases):
  111. databases = super().configure_settings(databases)
  112. if databases == {}:
  113. databases[DEFAULT_DB_ALIAS] = {'ENGINE': 'django.db.backends.dummy'}
  114. elif DEFAULT_DB_ALIAS not in databases:
  115. raise ImproperlyConfigured(
  116. f"You must define a '{DEFAULT_DB_ALIAS}' database."
  117. )
  118. elif databases[DEFAULT_DB_ALIAS] == {}:
  119. databases[DEFAULT_DB_ALIAS]['ENGINE'] = 'django.db.backends.dummy'
  120. return databases
  121. @property
  122. def databases(self):
  123. return self.settings
  124. def ensure_defaults(self, alias):
  125. """
  126. Put the defaults into the settings dictionary for a given connection
  127. where no settings is provided.
  128. """
  129. try:
  130. conn = self.databases[alias]
  131. except KeyError:
  132. raise self.exception_class(f"The connection '{alias}' doesn't exist.")
  133. conn.setdefault('ATOMIC_REQUESTS', False)
  134. conn.setdefault('AUTOCOMMIT', True)
  135. conn.setdefault('ENGINE', 'django.db.backends.dummy')
  136. if conn['ENGINE'] == 'django.db.backends.' or not conn['ENGINE']:
  137. conn['ENGINE'] = 'django.db.backends.dummy'
  138. conn.setdefault('CONN_MAX_AGE', 0)
  139. conn.setdefault('OPTIONS', {})
  140. conn.setdefault('TIME_ZONE', None)
  141. for setting in ['NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']:
  142. conn.setdefault(setting, '')
  143. def prepare_test_settings(self, alias):
  144. """
  145. Make sure the test settings are available in the 'TEST' sub-dictionary.
  146. """
  147. try:
  148. conn = self.databases[alias]
  149. except KeyError:
  150. raise self.exception_class(f"The connection '{alias}' doesn't exist.")
  151. test_settings = conn.setdefault('TEST', {})
  152. default_test_settings = [
  153. ('CHARSET', None),
  154. ('COLLATION', None),
  155. ('MIGRATE', True),
  156. ('MIRROR', None),
  157. ('NAME', None),
  158. ]
  159. for key, value in default_test_settings:
  160. test_settings.setdefault(key, value)
  161. def create_connection(self, alias):
  162. self.ensure_defaults(alias)
  163. self.prepare_test_settings(alias)
  164. db = self.databases[alias]
  165. backend = load_backend(db['ENGINE'])
  166. return backend.DatabaseWrapper(db, alias)
  167. def close_all(self):
  168. for alias in self:
  169. try:
  170. connection = getattr(self._connections, alias)
  171. except AttributeError:
  172. continue
  173. connection.close()
  174. class ConnectionRouter:
  175. def __init__(self, routers=None):
  176. """
  177. If routers is not specified, default to settings.DATABASE_ROUTERS.
  178. """
  179. self._routers = routers
  180. @cached_property
  181. def routers(self):
  182. if self._routers is None:
  183. self._routers = settings.DATABASE_ROUTERS
  184. routers = []
  185. for r in self._routers:
  186. if isinstance(r, str):
  187. router = import_string(r)()
  188. else:
  189. router = r
  190. routers.append(router)
  191. return routers
  192. def _router_func(action):
  193. def _route_db(self, model, **hints):
  194. chosen_db = None
  195. for router in self.routers:
  196. try:
  197. method = getattr(router, action)
  198. except AttributeError:
  199. # If the router doesn't have a method, skip to the next one.
  200. pass
  201. else:
  202. chosen_db = method(model, **hints)
  203. if chosen_db:
  204. return chosen_db
  205. instance = hints.get('instance')
  206. if instance is not None and instance._state.db:
  207. return instance._state.db
  208. return DEFAULT_DB_ALIAS
  209. return _route_db
  210. db_for_read = _router_func('db_for_read')
  211. db_for_write = _router_func('db_for_write')
  212. def allow_relation(self, obj1, obj2, **hints):
  213. for router in self.routers:
  214. try:
  215. method = router.allow_relation
  216. except AttributeError:
  217. # If the router doesn't have a method, skip to the next one.
  218. pass
  219. else:
  220. allow = method(obj1, obj2, **hints)
  221. if allow is not None:
  222. return allow
  223. return obj1._state.db == obj2._state.db
  224. def allow_migrate(self, db, app_label, **hints):
  225. for router in self.routers:
  226. try:
  227. method = router.allow_migrate
  228. except AttributeError:
  229. # If the router doesn't have a method, skip to the next one.
  230. continue
  231. allow = method(db, app_label, **hints)
  232. if allow is not None:
  233. return allow
  234. return True
  235. def allow_migrate_model(self, db, model):
  236. return self.allow_migrate(
  237. db,
  238. model._meta.app_label,
  239. model_name=model._meta.model_name,
  240. model=model,
  241. )
  242. def get_migratable_models(self, app_config, db, include_auto_created=False):
  243. """Return app models allowed to be migrated on provided db."""
  244. models = app_config.get_models(include_auto_created=include_auto_created)
  245. return [model for model in models if self.allow_migrate_model(db, model)]