From 2b09e4c88e96cb03b29f5a6b0e4838ab4271e631 Mon Sep 17 00:00:00 2001 From: Chandrakant Kumar Date: Sat, 28 Jan 2017 16:02:33 +0530 Subject: [PATCH] Fixed #27787 -- Made call_command() validate the options it receives. --- .../management/commands/createsuperuser.py | 1 + django/core/management/__init__.py | 14 ++++++++++++++ django/core/management/base.py | 9 +++++++++ django/core/management/commands/flush.py | 1 + django/core/management/commands/inspectdb.py | 3 +-- docs/releases/2.0.txt | 12 ++++++++++++ tests/auth_tests/test_management.py | 1 - tests/user_commands/tests.py | 19 +++++++++++++++++++ 8 files changed, 57 insertions(+), 3 deletions(-) diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index e43ba6725d..f9a198f0d4 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -20,6 +20,7 @@ class NotRunningInTTYException(Exception): class Command(BaseCommand): help = 'Used to create a superuser.' requires_migrations_checks = True + stealth_options = ('stdin',) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index c46c5e6767..872189d9f1 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -118,6 +118,20 @@ def call_command(command_name, *args, **options): arg_options = {opt_mapping.get(key, key): value for key, value in options.items()} defaults = parser.parse_args(args=[force_text(a) for a in args]) defaults = dict(defaults._get_kwargs(), **arg_options) + # Raise an error if any unknown options were passed. + stealth_options = set(command.base_stealth_options + command.stealth_options) + dest_parameters = {action.dest for action in parser._actions} + valid_options = dest_parameters | stealth_options | set(opt_mapping) + unknown_options = set(options) - valid_options + if unknown_options: + raise TypeError( + "Unknown option(s) for %s command: %s. " + "Valid options are: %s." % ( + command_name, + ', '.join(sorted(unknown_options)), + ', '.join(sorted(valid_options)), + ) + ) # Move positional args out of options to mimic legacy optparse args = defaults.pop('args', ()) if 'skip_checks' not in options: diff --git a/django/core/management/base.py b/django/core/management/base.py index 61ce6ebec5..bfc6811e28 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -183,6 +183,10 @@ class BaseCommand: that is locale-sensitive and such content shouldn't contain any translations (like it happens e.g. with django.contrib.auth permissions) as activating any locale might cause unintended effects. + + ``stealth_options`` + A tuple of any options the command uses which aren't defined by the + argument parser. """ # Metadata about this command. help = '' @@ -193,6 +197,11 @@ class BaseCommand: leave_locale_alone = False requires_migrations_checks = False requires_system_checks = True + # Arguments, common to all commands, which aren't defined by the argument + # parser. + base_stealth_options = ('skip_checks', 'stderr', 'stdout') + # Command-specific options not defined by the argument parser. + stealth_options = () def __init__(self, stdout=None, stderr=None, no_color=False): self.stdout = OutputWrapper(stdout or sys.stdout) diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py index 2a2b146107..e188e4613c 100644 --- a/django/core/management/commands/flush.py +++ b/django/core/management/commands/flush.py @@ -12,6 +12,7 @@ class Command(BaseCommand): 'Removes ALL DATA from the database, including data added during ' 'migrations. Does not achieve a "fresh install" state.' ) + stealth_options = ('reset_sequences', 'allow_cascade', 'inhibit_post_migrate') def add_arguments(self, parser): parser.add_argument( diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py index e335901973..dbcb5d1f38 100644 --- a/django/core/management/commands/inspectdb.py +++ b/django/core/management/commands/inspectdb.py @@ -9,9 +9,8 @@ from django.db.models.constants import LOOKUP_SEP class Command(BaseCommand): help = "Introspects the database tables in the given database and outputs a Django model module." - requires_system_checks = False - + stealth_options = ('table_name_filter', ) db_module = 'django.db' def add_arguments(self, parser): diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt index 6af7545199..9a7c207fd6 100644 --- a/docs/releases/2.0.txt +++ b/docs/releases/2.0.txt @@ -384,6 +384,18 @@ raises an exception and should be replaced with:: forms.IntegerField(max_value=25, min_value=10) +``call_command()`` validates the options it receives +---------------------------------------------------- + +``call_command()`` now validates that the argument parser of the command being +called defines all of the options passed to ``call_command()``. + +For custom management commands that use options not created using +``parser.add_argument()``, add a ``stealth_options`` attribute on the command:: + + class MyCommand(BaseCommand): + stealth_options = ('option_name', ...) + Miscellaneous ------------- diff --git a/tests/auth_tests/test_management.py b/tests/auth_tests/test_management.py index 9686ca1e2e..c4368b3bcf 100644 --- a/tests/auth_tests/test_management.py +++ b/tests/auth_tests/test_management.py @@ -320,7 +320,6 @@ class CreatesuperuserManagementCommandTestCase(TestCase): call_command( "createsuperuser", interactive=False, - username="joe@somewhere.org", stdout=new_io, stderr=new_io, ) diff --git a/tests/user_commands/tests.py b/tests/user_commands/tests.py index 6d11068f5b..19ae51b096 100644 --- a/tests/user_commands/tests.py +++ b/tests/user_commands/tests.py @@ -175,6 +175,25 @@ class CommandTests(SimpleTestCase): finally: dance.Command.requires_migrations_checks = requires_migrations_checks + def test_call_command_unrecognized_option(self): + msg = ( + 'Unknown option(s) for dance command: unrecognized. Valid options ' + 'are: example, help, integer, no_color, opt_3, option3, ' + 'pythonpath, settings, skip_checks, stderr, stdout, style, ' + 'traceback, verbosity, version.' + ) + with self.assertRaisesMessage(TypeError, msg): + management.call_command('dance', unrecognized=1) + + msg = ( + 'Unknown option(s) for dance command: unrecognized, unrecognized2. ' + 'Valid options are: example, help, integer, no_color, opt_3, ' + 'option3, pythonpath, settings, skip_checks, stderr, stdout, ' + 'style, traceback, verbosity, version.' + ) + with self.assertRaisesMessage(TypeError, msg): + management.call_command('dance', unrecognized=1, unrecognized2=1) + class CommandRunTests(AdminScriptTestCase): """