diff --git a/django/core/checks/__init__.py b/django/core/checks/__init__.py index cd21744327..657d0419f0 100644 --- a/django/core/checks/__init__.py +++ b/django/core/checks/__init__.py @@ -10,6 +10,7 @@ from .registry import Tags, register, run_checks, tag_exists # Import these to force registration of checks import django.core.checks.caches # NOQA isort:skip import django.core.checks.compatibility.django_1_8_0 # NOQA isort:skip +import django.core.checks.database # NOQA isort:skip import django.core.checks.model_checks # NOQA isort:skip import django.core.checks.security.base # NOQA isort:skip import django.core.checks.security.csrf # NOQA isort:skip diff --git a/django/core/checks/database.py b/django/core/checks/database.py new file mode 100644 index 0000000000..5778844fe9 --- /dev/null +++ b/django/core/checks/database.py @@ -0,0 +1,11 @@ +from django.db import connections + +from . import Tags, register + + +@register(Tags.database) +def check_database_backends(*args, **kwargs): + issues = [] + for conn in connections.all(): + issues.extend(conn.validation.check(**kwargs)) + return issues diff --git a/django/core/checks/registry.py b/django/core/checks/registry.py index 3e3d45fff8..292e0cb73b 100644 --- a/django/core/checks/registry.py +++ b/django/core/checks/registry.py @@ -13,6 +13,7 @@ class Tags(object): admin = 'admin' caches = 'caches' compatibility = 'compatibility' + database = 'database' models = 'models' security = 'security' signals = 'signals' @@ -70,6 +71,11 @@ class CheckRegistry(object): if tags is not None: checks = [check for check in checks if hasattr(check, 'tags') and set(check.tags) & set(tags)] + else: + # By default, 'database'-tagged checks are not run as they do more + # than mere static code analysis. + checks = [check for check in checks + if not hasattr(check, 'tags') or Tags.database not in check.tags] for check in checks: new_errors = check(app_configs=app_configs) diff --git a/django/core/management/base.py b/django/core/management/base.py index 1601d6d055..0a9037c1d1 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -368,6 +368,9 @@ class BaseCommand(object): translation.activate(saved_locale) return output + def _run_checks(self, **kwargs): + return checks.run_checks(**kwargs) + def check(self, app_configs=None, tags=None, display_num_errors=False, include_deployment_checks=False, fail_level=checks.ERROR): """ @@ -376,7 +379,7 @@ class BaseCommand(object): If there are only light messages (like warnings), they are printed to stderr and no exception is raised. """ - all_issues = checks.run_checks( + all_issues = self._run_checks( app_configs=app_configs, tags=tags, include_deployment_checks=include_deployment_checks, diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py index 84dbc59c65..72538f4586 100644 --- a/django/core/management/commands/migrate.py +++ b/django/core/management/commands/migrate.py @@ -6,6 +6,7 @@ from collections import OrderedDict from importlib import import_module from django.apps import apps +from django.core.checks import Tags, run_checks from django.core.management.base import BaseCommand, CommandError from django.core.management.sql import ( emit_post_migrate_signal, emit_pre_migrate_signal, @@ -56,6 +57,11 @@ class Command(BaseCommand): help='Creates tables for apps without migrations.', ) + def _run_checks(self, **kwargs): + issues = run_checks(tags=[Tags.database]) + issues.extend(super(Command, self).check(**kwargs)) + return issues + def handle(self, *args, **options): self.verbosity = options['verbosity'] diff --git a/django/db/backends/base/validation.py b/django/db/backends/base/validation.py index 888908c1aa..23761b4161 100644 --- a/django/db/backends/base/validation.py +++ b/django/db/backends/base/validation.py @@ -1,9 +1,12 @@ class BaseDatabaseValidation(object): """ - This class encapsulates all backend-specific model validation. + This class encapsulates all backend-specific validation. """ def __init__(self, connection): self.connection = connection + def check(self, **kwargs): + return [] + def check_field(self, field, **kwargs): return [] diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index c82bd2bc98..a332df2071 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -84,6 +84,14 @@ Django's system checks are organized using the following tags: * ``templates``: Checks template related configuration. * ``caches``: Checks cache related configuration. * ``urls``: Checks URL configuration. +* ``database``: Checks database-related configuration issues. Database checks + are not run by default because they do more than static code analysis as + regular checks do. They are only run by the :djadmin:`migrate` command or if + you specify the ``database`` tag when calling the :djadmin:`check` command. + +.. versionadded:: 1.10 + + The ``database`` tag was added. Some checks may be registered with multiple tags. diff --git a/docs/topics/checks.txt b/docs/topics/checks.txt index a019caa7c6..355e85714e 100644 --- a/docs/topics/checks.txt +++ b/docs/topics/checks.txt @@ -120,17 +120,22 @@ The code below is equivalent to the code above:: .. _field-checking: -Field, model, and manager checks --------------------------------- +Field, model, manager, and database checks +------------------------------------------ In some cases, you won't need to register your check function -- you can piggyback on an existing registration. -Fields, models, and model managers all implement a ``check()`` method that is -already registered with the check framework. If you want to add extra checks, -you can extend the implementation on the base class, perform any extra -checks you need, and append any messages to those generated by the base class. -It's recommended that you delegate each check to separate methods. +Fields, models, model managers, and database backends all implement a +``check()`` method that is already registered with the check framework. If you +want to add extra checks, you can extend the implementation on the base class, +perform any extra checks you need, and append any messages to those generated +by the base class. It's recommended that you delegate each check to separate +methods. + +.. versionchanged:: 1.10 + + Database backend checks were added. Consider an example where you are implementing a custom field named ``RangedIntegerField``. This field adds ``min`` and ``max`` arguments to the diff --git a/tests/check_framework/test_database.py b/tests/check_framework/test_database.py new file mode 100644 index 0000000000..73edd6234c --- /dev/null +++ b/tests/check_framework/test_database.py @@ -0,0 +1,33 @@ +import unittest + +from django.core.checks import Tags, run_checks +from django.core.checks.registry import CheckRegistry +from django.db import connection +from django.test import TestCase, mock + + +class DatabaseCheckTests(TestCase): + @property + def func(self): + from django.core.checks.database import check_database_backends + return check_database_backends + + def test_database_checks_not_run_by_default(self): + """ + `database` checks are only run when their tag is specified. + """ + def f1(**kwargs): + return [5] + + registry = CheckRegistry() + registry.register(Tags.database)(f1) + errors = registry.run_checks() + self.assertEqual(errors, []) + + errors2 = registry.run_checks(tags=[Tags.database]) + self.assertEqual(errors2, [5]) + + def test_database_checks_called(self): + with mock.patch('django.db.backends.base.validation.BaseDatabaseValidation.check') as mocked_check: + run_checks(tags=[Tags.database]) + self.assertTrue(mocked_check.called)