Fixed #31546 -- Allowed specifying list of tags in Command.requires_system_checks.

This commit is contained in:
Hasan Ramezani 2020-05-14 00:00:41 +02:00 committed by Carlton Gibson
parent a4e6030904
commit c60524c658
30 changed files with 156 additions and 41 deletions

View File

@ -12,7 +12,7 @@ UserModel = get_user_model()
class Command(BaseCommand): class Command(BaseCommand):
help = "Change a user's password for django.contrib.auth." help = "Change a user's password for django.contrib.auth."
requires_migrations_checks = True requires_migrations_checks = True
requires_system_checks = False requires_system_checks = []
def _get_pass(self, prompt="Password: "): def _get_pass(self, prompt="Password: "):
p = getpass.getpass(prompt=prompt) p = getpass.getpass(prompt=prompt)

View File

@ -37,7 +37,7 @@ class Command(BaseCommand):
' ./manage.py ogrinspect zipcode.shp Zipcode' ' ./manage.py ogrinspect zipcode.shp Zipcode'
) )
requires_system_checks = False requires_system_checks = []
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument('data_source', help='Path to the data source.') parser.add_argument('data_source', help='Path to the data source.')

View File

@ -16,7 +16,7 @@ class Command(BaseCommand):
settings.STATIC_ROOT. settings.STATIC_ROOT.
""" """
help = "Collect static files in a single location." help = "Collect static files in a single location."
requires_system_checks = False requires_system_checks = [Tags.staticfiles]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -36,10 +36,6 @@ class Command(BaseCommand):
return True return True
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument(
'--skip-checks', action='store_true',
help='Skip system checks.',
)
parser.add_argument( parser.add_argument(
'--noinput', '--no-input', action='store_false', dest='interactive', '--noinput', '--no-input', action='store_false', dest='interactive',
help="Do NOT prompt the user for input of any kind.", help="Do NOT prompt the user for input of any kind.",
@ -151,9 +147,6 @@ class Command(BaseCommand):
def handle(self, **options): def handle(self, **options):
self.set_options(**options) self.set_options(**options)
if not options['skip_checks']:
self.check(tags=[Tags.staticfiles])
message = ['\n'] message = ['\n']
if self.dry_run: if self.dry_run:
message.append( message.append(

View File

@ -4,6 +4,7 @@ be executed through ``django-admin`` or ``manage.py``).
""" """
import os import os
import sys import sys
import warnings
from argparse import ArgumentParser, HelpFormatter from argparse import ArgumentParser, HelpFormatter
from io import TextIOBase from io import TextIOBase
@ -12,6 +13,9 @@ from django.core import checks
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.management.color import color_style, no_style from django.core.management.color import color_style, no_style
from django.db import DEFAULT_DB_ALIAS, connections from django.db import DEFAULT_DB_ALIAS, connections
from django.utils.deprecation import RemovedInDjango41Warning
ALL_CHECKS = '__all__'
class CommandError(Exception): class CommandError(Exception):
@ -203,8 +207,11 @@ class BaseCommand:
migrations on disk don't match the migrations in the database. migrations on disk don't match the migrations in the database.
``requires_system_checks`` ``requires_system_checks``
A boolean; if ``True``, entire Django project will be checked for errors A list or tuple of tags, e.g. [Tags.staticfiles, Tags.models]. System
prior to executing the command. Default value is ``True``. checks registered in the chosen tags will be checked for errors prior
to executing the command. The value '__all__' can be used to specify
that all system checks should be performed. Default value is '__all__'.
To validate an individual application's models To validate an individual application's models
rather than all applications' models, call rather than all applications' models, call
``self.check(app_configs)`` from ``handle()``, where ``app_configs`` ``self.check(app_configs)`` from ``handle()``, where ``app_configs``
@ -222,7 +229,7 @@ class BaseCommand:
_called_from_command_line = False _called_from_command_line = False
output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;" output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;"
requires_migrations_checks = False requires_migrations_checks = False
requires_system_checks = True requires_system_checks = '__all__'
# Arguments, common to all commands, which aren't defined by the argument # Arguments, common to all commands, which aren't defined by the argument
# parser. # parser.
base_stealth_options = ('stderr', 'stdout') base_stealth_options = ('stderr', 'stdout')
@ -239,6 +246,19 @@ class BaseCommand:
else: else:
self.style = color_style(force_color) self.style = color_style(force_color)
self.stderr.style_func = self.style.ERROR self.stderr.style_func = self.style.ERROR
if self.requires_system_checks in [False, True]:
warnings.warn(
"Using a boolean value for requires_system_checks is "
"deprecated. Use '__all__' instead of True, and [] (an empty "
"list) instead of False.",
RemovedInDjango41Warning,
)
self.requires_system_checks = ALL_CHECKS if self.requires_system_checks else []
if (
not isinstance(self.requires_system_checks, (list, tuple)) and
self.requires_system_checks != ALL_CHECKS
):
raise TypeError('requires_system_checks must be a list or tuple.')
def get_version(self): def get_version(self):
""" """
@ -365,7 +385,10 @@ class BaseCommand:
self.stderr = OutputWrapper(options['stderr']) self.stderr = OutputWrapper(options['stderr'])
if self.requires_system_checks and not options['skip_checks']: if self.requires_system_checks and not options['skip_checks']:
if self.requires_system_checks == ALL_CHECKS:
self.check() self.check()
else:
self.check(tags=self.requires_system_checks)
if self.requires_migrations_checks: if self.requires_migrations_checks:
self.check_migrations() self.check_migrations()
output = self.handle(*args, **options) output = self.handle(*args, **options)

View File

@ -7,7 +7,7 @@ from django.core.management.base import BaseCommand, CommandError
class Command(BaseCommand): class Command(BaseCommand):
help = "Checks the entire Django project for potential problems." help = "Checks the entire Django project for potential problems."
requires_system_checks = False requires_system_checks = []
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument('args', metavar='app_label', nargs='*') parser.add_argument('args', metavar='app_label', nargs='*')

View File

@ -29,7 +29,7 @@ def is_writable(path):
class Command(BaseCommand): class Command(BaseCommand):
help = 'Compiles .po files to .mo files for use with builtin gettext support.' help = 'Compiles .po files to .mo files for use with builtin gettext support.'
requires_system_checks = False requires_system_checks = []
program = 'msgfmt' program = 'msgfmt'
program_options = ['--check-format'] program_options = ['--check-format']

View File

@ -10,7 +10,7 @@ from django.db import (
class Command(BaseCommand): class Command(BaseCommand):
help = "Creates the tables needed to use the SQL cache backend." help = "Creates the tables needed to use the SQL cache backend."
requires_system_checks = False requires_system_checks = []
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(

View File

@ -10,7 +10,7 @@ class Command(BaseCommand):
"default database if none is provided." "default database if none is provided."
) )
requires_system_checks = False requires_system_checks = []
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(

View File

@ -10,7 +10,7 @@ class Command(BaseCommand):
help = """Displays differences between the current settings.py and Django's help = """Displays differences between the current settings.py and Django's
default settings.""" default settings."""
requires_system_checks = False requires_system_checks = []
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(

View File

@ -8,7 +8,7 @@ from django.db.models.constants import LOOKUP_SEP
class Command(BaseCommand): class Command(BaseCommand):
help = "Introspects the database tables in the given database and outputs a Django model module." help = "Introspects the database tables in the given database and outputs a Django model module."
requires_system_checks = False requires_system_checks = []
stealth_options = ('table_name_filter',) stealth_options = ('table_name_filter',)
db_module = 'django.db' db_module = 'django.db'

View File

@ -206,7 +206,7 @@ class Command(BaseCommand):
translatable_file_class = TranslatableFile translatable_file_class = TranslatableFile
build_file_class = BuildFile build_file_class = BuildFile
requires_system_checks = False requires_system_checks = []
msgmerge_options = ['-q', '--previous'] msgmerge_options = ['-q', '--previous']
msguniq_options = ['--to-code=utf-8'] msguniq_options = ['--to-code=utf-8']

View File

@ -20,7 +20,7 @@ from django.utils.text import Truncator
class Command(BaseCommand): class Command(BaseCommand):
help = "Updates database schema. Manages both apps with migrations and those without." help = "Updates database schema. Manages both apps with migrations and those without."
requires_system_checks = False requires_system_checks = []
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(

View File

@ -25,7 +25,7 @@ class Command(BaseCommand):
help = "Starts a lightweight Web server for development." help = "Starts a lightweight Web server for development."
# Validation is called explicitly each time the server is reloaded. # Validation is called explicitly each time the server is reloaded.
requires_system_checks = False requires_system_checks = []
stealth_options = ('shutdown_message',) stealth_options = ('shutdown_message',)
default_addr = '127.0.0.1' default_addr = '127.0.0.1'

View File

@ -14,7 +14,7 @@ class Command(BaseCommand):
"as code." "as code."
) )
requires_system_checks = False requires_system_checks = []
shells = ['ipython', 'bpython', 'python'] shells = ['ipython', 'bpython', 'python']
def add_arguments(self, parser): def add_arguments(self, parser):

View File

@ -10,7 +10,7 @@ class Command(BaseCommand):
help = 'Discover and run tests in the specified modules or the current directory.' help = 'Discover and run tests in the specified modules or the current directory.'
# DiscoverRunner runs the checks after databases are set up. # DiscoverRunner runs the checks after databases are set up.
requires_system_checks = False requires_system_checks = []
test_runner = None test_runner = None
def run_from_argv(self, argv): def run_from_argv(self, argv):

View File

@ -6,7 +6,7 @@ from django.db import connection
class Command(BaseCommand): class Command(BaseCommand):
help = 'Runs a development server with data from the given fixture(s).' help = 'Runs a development server with data from the given fixture(s).'
requires_system_checks = False requires_system_checks = []
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(

View File

@ -28,7 +28,7 @@ class TemplateCommand(BaseCommand):
:param directory: The directory to which the template should be copied. :param directory: The directory to which the template should be copied.
:param options: The additional variables passed to project or app templates :param options: The additional variables passed to project or app templates
""" """
requires_system_checks = False requires_system_checks = []
# The supported URL schemes # The supported URL schemes
url_schemes = ['http', 'https', 'ftp'] url_schemes = ['http', 'https', 'ftp']
# Rewrite the following suffixes when determining the target filename. # Rewrite the following suffixes when determining the target filename.

View File

@ -215,8 +215,15 @@ All attributes can be set in your derived class and can be used in
.. attribute:: BaseCommand.requires_system_checks .. attribute:: BaseCommand.requires_system_checks
A boolean; if ``True``, the entire Django project will be checked for A list or tuple of tags, e.g. ``[Tags.staticfiles, Tags.models]``. System
potential problems prior to executing the command. Default value is ``True``. checks registered in the chosen tags will be checked for errors prior to
executing the command. The value ``'__all__'`` can be used to specify
that all system checks should be performed. Default value is ``'__all__'``.
.. versionchanged:: 3.2
In older versions, the ``requires_system_checks`` attribute expects a
boolean value instead of a list or tuple of tags.
.. attribute:: BaseCommand.style .. attribute:: BaseCommand.style

View File

@ -19,6 +19,8 @@ details on these changes.
``copy.deepcopy()`` to class attributes in ``TestCase.setUpTestData()`` will ``copy.deepcopy()`` to class attributes in ``TestCase.setUpTestData()`` will
be removed. be removed.
* ``BaseCommand.requires_system_checks`` won't support boolean values.
.. _deprecation-removed-in-4.0: .. _deprecation-removed-in-4.0:
4.0 4.0

View File

@ -1826,7 +1826,7 @@ colored output to another command.
Skips running system checks prior to running the command. This option is only Skips running system checks prior to running the command. This option is only
available if the available if the
:attr:`~django.core.management.BaseCommand.requires_system_checks` command :attr:`~django.core.management.BaseCommand.requires_system_checks` command
attribute is set to ``True``. attribute is not an empty list or tuple.
Example usage:: Example usage::

View File

@ -161,6 +161,11 @@ Management Commands
connection. In that case, check for a consistent migration history is connection. In that case, check for a consistent migration history is
skipped. skipped.
* :attr:`.BaseCommand.requires_system_checks` now supports specifying a list of
tags. System checks registered in the chosen tags will be checked for errors
prior to executing the command. In previous versions, either all or none
of the system checks were performed.
Migrations Migrations
~~~~~~~~~~ ~~~~~~~~~~
@ -273,3 +278,7 @@ Miscellaneous
* Assigning objects which don't support creating deep copies with * Assigning objects which don't support creating deep copies with
:py:func:`copy.deepcopy` to class attributes in :py:func:`copy.deepcopy` to class attributes in
:meth:`.TestCase.setUpTestData` is deprecated. :meth:`.TestCase.setUpTestData` is deprecated.
* Using a boolean value in :attr:`.BaseCommand.requires_system_checks` is
deprecated. Use ``'__all__'`` instead of ``True``, and ``[]`` (an empty list)
instead of ``False``.

View File

@ -3,7 +3,7 @@ from django.core.management.base import AppCommand
class Command(AppCommand): class Command(AppCommand):
help = 'Test Application-based commands' help = 'Test Application-based commands'
requires_system_checks = False requires_system_checks = []
def handle_app_config(self, app_config, **options): def handle_app_config(self, app_config, **options):
print('EXECUTE:AppCommand name=%s, options=%s' % (app_config.name, sorted(options.items()))) print('EXECUTE:AppCommand name=%s, options=%s' % (app_config.name, sorted(options.items())))

View File

@ -3,7 +3,7 @@ from django.core.management.base import BaseCommand
class Command(BaseCommand): class Command(BaseCommand):
help = 'Test basic commands' help = 'Test basic commands'
requires_system_checks = False requires_system_checks = []
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument('args', nargs='*') parser.add_argument('args', nargs='*')

View File

@ -3,7 +3,7 @@ from django.core.management.base import LabelCommand
class Command(LabelCommand): class Command(LabelCommand):
help = "Test Label-based commands" help = "Test Label-based commands"
requires_system_checks = False requires_system_checks = []
def handle_label(self, label, **options): def handle_label(self, label, **options):
print('EXECUTE:LabelCommand label=%s, options=%s' % (label, sorted(options.items()))) print('EXECUTE:LabelCommand label=%s, options=%s' % (label, sorted(options.items())))

View File

@ -3,7 +3,7 @@ from django.core.management.base import BaseCommand
class Command(BaseCommand): class Command(BaseCommand):
help = "Test No-args commands" help = "Test No-args commands"
requires_system_checks = False requires_system_checks = []
def handle(self, **options): def handle(self, **options):
print('EXECUTE: noargs_command options=%s' % sorted(options.items())) print('EXECUTE: noargs_command options=%s' % sorted(options.items()))

View File

@ -1395,7 +1395,7 @@ class ManageTestserver(SimpleTestCase):
# the commands are correctly parsed and processed. # the commands are correctly parsed and processed.
########################################################################## ##########################################################################
class ColorCommand(BaseCommand): class ColorCommand(BaseCommand):
requires_system_checks = False requires_system_checks = []
def handle(self, *args, **options): def handle(self, *args, **options):
self.stdout.write('Hello, world!', self.style.ERROR) self.stdout.write('Hello, world!', self.style.ERROR)
@ -1541,7 +1541,7 @@ class CommandTypes(AdminScriptTestCase):
def test_custom_stdout(self): def test_custom_stdout(self):
class Command(BaseCommand): class Command(BaseCommand):
requires_system_checks = False requires_system_checks = []
def handle(self, *args, **options): def handle(self, *args, **options):
self.stdout.write("Hello, World!") self.stdout.write("Hello, World!")
@ -1558,7 +1558,7 @@ class CommandTypes(AdminScriptTestCase):
def test_custom_stderr(self): def test_custom_stderr(self):
class Command(BaseCommand): class Command(BaseCommand):
requires_system_checks = False requires_system_checks = []
def handle(self, *args, **options): def handle(self, *args, **options):
self.stderr.write("Hello, World!") self.stderr.write("Hello, World!")

View File

@ -4,7 +4,7 @@ from django.core.management.base import BaseCommand, CommandError
class Command(BaseCommand): class Command(BaseCommand):
help = "Dance around like a madman." help = "Dance around like a madman."
args = '' args = ''
requires_system_checks = True requires_system_checks = '__all__'
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument("integer", nargs='?', type=int, default=0) parser.add_argument("integer", nargs='?', type=int, default=0)

View File

@ -0,0 +1,8 @@
from django.core.management.base import BaseCommand
class Command(BaseCommand):
requires_system_checks = []
def handle(self, *args, **options):
pass

View File

@ -0,0 +1,9 @@
from django.core.checks import Tags
from django.core.management.base import BaseCommand
class Command(BaseCommand):
requires_system_checks = [Tags.staticfiles, Tags.models]
def handle(self, *args, **options):
pass

View File

@ -6,6 +6,7 @@ from admin_scripts.tests import AdminScriptTestCase
from django.apps import apps from django.apps import apps
from django.core import management from django.core import management
from django.core.checks import Tags
from django.core.management import BaseCommand, CommandError, find_commands from django.core.management import BaseCommand, CommandError, find_commands
from django.core.management.utils import ( from django.core.management.utils import (
find_command, get_random_secret_key, is_ignored_path, find_command, get_random_secret_key, is_ignored_path,
@ -13,8 +14,9 @@ from django.core.management.utils import (
) )
from django.db import connection from django.db import connection
from django.test import SimpleTestCase, override_settings from django.test import SimpleTestCase, override_settings
from django.test.utils import captured_stderr, extend_sys_path from django.test.utils import captured_stderr, extend_sys_path, ignore_warnings
from django.utils import translation from django.utils import translation
from django.utils.deprecation import RemovedInDjango41Warning
from django.utils.version import PY37 from django.utils.version import PY37
from .management.commands import dance from .management.commands import dance
@ -59,13 +61,13 @@ class CommandTests(SimpleTestCase):
with self.assertRaises(CommandError) as cm: with self.assertRaises(CommandError) as cm:
management.call_command('dance', example="raise") management.call_command('dance', example="raise")
self.assertEqual(cm.exception.returncode, 3) self.assertEqual(cm.exception.returncode, 3)
dance.Command.requires_system_checks = False dance.Command.requires_system_checks = []
try: try:
with captured_stderr() as stderr, self.assertRaises(SystemExit) as cm: with captured_stderr() as stderr, self.assertRaises(SystemExit) as cm:
management.ManagementUtility(['manage.py', 'dance', '--example=raise']).execute() management.ManagementUtility(['manage.py', 'dance', '--example=raise']).execute()
self.assertEqual(cm.exception.code, 3) self.assertEqual(cm.exception.code, 3)
finally: finally:
dance.Command.requires_system_checks = True dance.Command.requires_system_checks = '__all__'
self.assertIn("CommandError", stderr.getvalue()) self.assertIn("CommandError", stderr.getvalue())
def test_no_translations_deactivate_translations(self): def test_no_translations_deactivate_translations(self):
@ -155,6 +157,7 @@ class CommandTests(SimpleTestCase):
def patched_check(self_, **kwargs): def patched_check(self_, **kwargs):
self.counter += 1 self.counter += 1
self.kwargs = kwargs
saved_check = BaseCommand.check saved_check = BaseCommand.check
BaseCommand.check = patched_check BaseCommand.check = patched_check
@ -163,9 +166,28 @@ class CommandTests(SimpleTestCase):
self.assertEqual(self.counter, 0) self.assertEqual(self.counter, 0)
management.call_command("dance", verbosity=0, skip_checks=False) management.call_command("dance", verbosity=0, skip_checks=False)
self.assertEqual(self.counter, 1) self.assertEqual(self.counter, 1)
self.assertEqual(self.kwargs, {})
finally: finally:
BaseCommand.check = saved_check BaseCommand.check = saved_check
def test_requires_system_checks_empty(self):
with mock.patch('django.core.management.base.BaseCommand.check') as mocked_check:
management.call_command('no_system_checks')
self.assertIs(mocked_check.called, False)
def test_requires_system_checks_specific(self):
with mock.patch('django.core.management.base.BaseCommand.check') as mocked_check:
management.call_command('specific_system_checks')
mocked_check.called_once_with(tags=[Tags.staticfiles, Tags.models])
def test_requires_system_checks_invalid(self):
class Command(BaseCommand):
requires_system_checks = 'x'
msg = 'requires_system_checks must be a list or tuple.'
with self.assertRaisesMessage(TypeError, msg):
Command()
def test_check_migrations(self): def test_check_migrations(self):
requires_migrations_checks = dance.Command.requires_migrations_checks requires_migrations_checks = dance.Command.requires_migrations_checks
self.assertIs(requires_migrations_checks, False) self.assertIs(requires_migrations_checks, False)
@ -334,3 +356,45 @@ class UtilsTests(SimpleTestCase):
def test_normalize_path_patterns_truncates_wildcard_base(self): def test_normalize_path_patterns_truncates_wildcard_base(self):
expected = [os.path.normcase(p) for p in ['foo/bar', 'bar/*/']] expected = [os.path.normcase(p) for p in ['foo/bar', 'bar/*/']]
self.assertEqual(normalize_path_patterns(['foo/bar/*', 'bar/*/']), expected) self.assertEqual(normalize_path_patterns(['foo/bar/*', 'bar/*/']), expected)
class DeprecationTests(SimpleTestCase):
def test_requires_system_checks_warning(self):
class Command(BaseCommand):
pass
msg = (
"Using a boolean value for requires_system_checks is deprecated. "
"Use '__all__' instead of True, and [] (an empty list) instead of "
"False."
)
for value in [False, True]:
Command.requires_system_checks = value
with self.assertRaisesMessage(RemovedInDjango41Warning, msg):
Command()
@ignore_warnings(category=RemovedInDjango41Warning)
def test_requires_system_checks_true(self):
class Command(BaseCommand):
requires_system_checks = True
def handle(self, *args, **options):
pass
command = Command()
with mock.patch('django.core.management.base.BaseCommand.check') as mocked_check:
management.call_command(command, skip_checks=False)
mocked_check.assert_called_once_with()
@ignore_warnings(category=RemovedInDjango41Warning)
def test_requires_system_checks_false(self):
class Command(BaseCommand):
requires_system_checks = False
def handle(self, *args, **options):
pass
command = Command()
with mock.patch('django.core.management.base.BaseCommand.check') as mocked_check:
management.call_command(command)
self.assertIs(mocked_check.called, False)