diff --git a/django/core/management/base.py b/django/core/management/base.py index 463e4e8665e..2044b857540 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -2,6 +2,7 @@ Base classes for writing management commands (named commands which can be executed through ``django-admin`` or ``manage.py``). """ +import argparse import os import sys import warnings @@ -239,6 +240,7 @@ class BaseCommand: base_stealth_options = ('stderr', 'stdout') # Command-specific options not defined by the argument parser. stealth_options = () + suppressed_base_arguments = set() def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False): self.stdout = OutputWrapper(stdout or sys.stdout) @@ -285,31 +287,37 @@ class BaseCommand: called_from_command_line=getattr(self, '_called_from_command_line', None), **kwargs ) - parser.add_argument('--version', action='version', version=self.get_version()) - parser.add_argument( - '-v', '--verbosity', default=1, + self.add_base_argument( + parser, '--version', action='version', version=self.get_version(), + help="Show program's version number and exit.", + ) + self.add_base_argument( + parser, '-v', '--verbosity', default=1, type=int, choices=[0, 1, 2, 3], help='Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output', ) - parser.add_argument( - '--settings', + self.add_base_argument( + parser, '--settings', help=( 'The Python path to a settings module, e.g. ' '"myproject.settings.main". If this isn\'t provided, the ' 'DJANGO_SETTINGS_MODULE environment variable will be used.' ), ) - parser.add_argument( - '--pythonpath', + self.add_base_argument( + parser, '--pythonpath', help='A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".', ) - parser.add_argument('--traceback', action='store_true', help='Raise on CommandError exceptions') - parser.add_argument( - '--no-color', action='store_true', + self.add_base_argument( + parser, '--traceback', action='store_true', + help='Raise on CommandError exceptions.', + ) + self.add_base_argument( + parser, '--no-color', action='store_true', help="Don't colorize the command output.", ) - parser.add_argument( - '--force-color', action='store_true', + self.add_base_argument( + parser, '--force-color', action='store_true', help='Force colorization of the command output.', ) if self.requires_system_checks: @@ -326,6 +334,17 @@ class BaseCommand: """ pass + def add_base_argument(self, parser, *args, **kwargs): + """ + Call the parser's add_argument() method, suppressing the help text + according to BaseCommand.suppressed_base_arguments. + """ + for arg in args: + if arg in self.suppressed_base_arguments: + kwargs['help'] = argparse.SUPPRESS + break + parser.add_argument(*args, **kwargs) + def print_help(self, prog_name, subcommand): """ Print the help message for this command, derived from diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py index 66f5217820d..473fde0de06 100644 --- a/django/core/management/commands/runserver.py +++ b/django/core/management/commands/runserver.py @@ -27,6 +27,7 @@ class Command(BaseCommand): # Validation is called explicitly each time the server is reloaded. requires_system_checks = [] stealth_options = ('shutdown_message',) + suppressed_base_arguments = {'--verbosity', '--traceback'} default_addr = '127.0.0.1' default_addr_ipv6 = '::1' diff --git a/docs/howto/custom-management-commands.txt b/docs/howto/custom-management-commands.txt index 3ecde33d677..0cb06a826d8 100644 --- a/docs/howto/custom-management-commands.txt +++ b/docs/howto/custom-management-commands.txt @@ -242,6 +242,14 @@ All attributes can be set in your derived class and can be used in If you pass the :option:`--no-color` option when running your command, all ``self.style()`` calls will return the original string uncolored. +.. attribute:: BaseCommand.suppressed_base_arguments + + .. versionadded:: 4.0 + + The default command options to suppress in the help output. This should be + a set of option names (e.g. ``'--verbosity'``). The default values for the + suppressed options are still passed. + Methods ------- diff --git a/docs/releases/4.0.txt b/docs/releases/4.0.txt index 53b738c8ec4..5849b08cd43 100644 --- a/docs/releases/4.0.txt +++ b/docs/releases/4.0.txt @@ -274,6 +274,10 @@ Management Commands As a consequence, ``readline`` is no longer loaded if running in *isolated* mode. +* The new :attr:`BaseCommand.suppressed_base_arguments + ` attribute + allows suppressing unsupported default command options in the help output. + Migrations ~~~~~~~~~~ diff --git a/tests/admin_scripts/management/commands/suppress_base_options_command.py b/tests/admin_scripts/management/commands/suppress_base_options_command.py new file mode 100644 index 00000000000..769ef3178cd --- /dev/null +++ b/tests/admin_scripts/management/commands/suppress_base_options_command.py @@ -0,0 +1,24 @@ +from django.core.management import BaseCommand + + +class Command(BaseCommand): + + help = 'Test suppress base options command.' + requires_system_checks = [] + suppressed_base_arguments = { + '-v', + '--traceback', + '--settings', + '--pythonpath', + '--no-color', + '--force-color', + '--version', + 'file', + } + + def add_arguments(self, parser): + super().add_arguments(parser) + self.add_base_argument(parser, 'file', nargs='?', help='input file') + + def handle(self, *labels, **options): + print('EXECUTE:SuppressBaseOptionsCommand options=%s' % sorted(options.items())) diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index e5216e93e9c..a3ceb8ad751 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -1381,6 +1381,15 @@ class ManageRunserverEmptyAllowedHosts(AdminScriptTestCase): self.assertOutput(err, 'CommandError: You must set settings.ALLOWED_HOSTS if DEBUG is False.') +class ManageRunserverHelpOutput(AdminScriptTestCase): + def test_suppressed_options(self): + """runserver doesn't support --verbosity and --trackback options.""" + out, err = self.run_manage(['runserver', '--help']) + self.assertNotInOutput(out, '--verbosity') + self.assertNotInOutput(out, '--trackback') + self.assertOutput(out, '--settings') + + class ManageTestserver(SimpleTestCase): @mock.patch.object(TestserverCommand, 'handle', return_value='') @@ -1847,6 +1856,34 @@ class CommandTypes(AdminScriptTestCase): "('settings', None), ('traceback', False), ('verbosity', 1)]" ) + def test_suppress_base_options_command_help(self): + args = ['suppress_base_options_command', '--help'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, 'Test suppress base options command.') + self.assertNotInOutput(out, 'input file') + self.assertOutput(out, '-h, --help') + self.assertNotInOutput(out, '--version') + self.assertNotInOutput(out, '--verbosity') + self.assertNotInOutput(out, '-v {0,1,2,3}') + self.assertNotInOutput(out, '--settings') + self.assertNotInOutput(out, '--pythonpath') + self.assertNotInOutput(out, '--traceback') + self.assertNotInOutput(out, '--no-color') + self.assertNotInOutput(out, '--force-color') + + def test_suppress_base_options_command_defaults(self): + args = ['suppress_base_options_command'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput( + out, + "EXECUTE:SuppressBaseOptionsCommand options=[('file', None), " + "('force_color', False), ('no_color', False), " + "('pythonpath', None), ('settings', None), " + "('traceback', False), ('verbosity', 1)]" + ) + class Discovery(SimpleTestCase):