Fixed #27787 -- Made call_command() validate the options it receives.

This commit is contained in:
Chandrakant Kumar 2017-01-28 16:02:33 +05:30 committed by Tim Graham
parent 92e286498a
commit 2b09e4c88e
8 changed files with 57 additions and 3 deletions

View File

@ -20,6 +20,7 @@ class NotRunningInTTYException(Exception):
class Command(BaseCommand): class Command(BaseCommand):
help = 'Used to create a superuser.' help = 'Used to create a superuser.'
requires_migrations_checks = True requires_migrations_checks = True
stealth_options = ('stdin',)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -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()} 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 = parser.parse_args(args=[force_text(a) for a in args])
defaults = dict(defaults._get_kwargs(), **arg_options) 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 # Move positional args out of options to mimic legacy optparse
args = defaults.pop('args', ()) args = defaults.pop('args', ())
if 'skip_checks' not in options: if 'skip_checks' not in options:

View File

@ -183,6 +183,10 @@ class BaseCommand:
that is locale-sensitive and such content shouldn't contain any that is locale-sensitive and such content shouldn't contain any
translations (like it happens e.g. with django.contrib.auth translations (like it happens e.g. with django.contrib.auth
permissions) as activating any locale might cause unintended effects. 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. # Metadata about this command.
help = '' help = ''
@ -193,6 +197,11 @@ class BaseCommand:
leave_locale_alone = False leave_locale_alone = False
requires_migrations_checks = False requires_migrations_checks = False
requires_system_checks = True 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): def __init__(self, stdout=None, stderr=None, no_color=False):
self.stdout = OutputWrapper(stdout or sys.stdout) self.stdout = OutputWrapper(stdout or sys.stdout)

View File

@ -12,6 +12,7 @@ class Command(BaseCommand):
'Removes ALL DATA from the database, including data added during ' 'Removes ALL DATA from the database, including data added during '
'migrations. Does not achieve a "fresh install" state.' 'migrations. Does not achieve a "fresh install" state.'
) )
stealth_options = ('reset_sequences', 'allow_cascade', 'inhibit_post_migrate')
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(

View File

@ -9,9 +9,8 @@ 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 = False
stealth_options = ('table_name_filter', )
db_module = 'django.db' db_module = 'django.db'
def add_arguments(self, parser): def add_arguments(self, parser):

View File

@ -384,6 +384,18 @@ raises an exception and should be replaced with::
forms.IntegerField(max_value=25, min_value=10) 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 Miscellaneous
------------- -------------

View File

@ -320,7 +320,6 @@ class CreatesuperuserManagementCommandTestCase(TestCase):
call_command( call_command(
"createsuperuser", "createsuperuser",
interactive=False, interactive=False,
username="joe@somewhere.org",
stdout=new_io, stdout=new_io,
stderr=new_io, stderr=new_io,
) )

View File

@ -175,6 +175,25 @@ class CommandTests(SimpleTestCase):
finally: finally:
dance.Command.requires_migrations_checks = requires_migrations_checks 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): class CommandRunTests(AdminScriptTestCase):
""" """