summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChandrakant Kumar <k.03chandra@gmail.com>2017-01-28 16:02:33 +0530
committerTim Graham <timograham@gmail.com>2017-06-16 21:28:38 -0400
commit2b09e4c88e96cb03b29f5a6b0e4838ab4271e631 (patch)
treee5c7322c72e80add326cce75dc3b5cfbc3f52189
parent92e286498acd4f4562be11c21879856de8f47252 (diff)
Fixed #27787 -- Made call_command() validate the options it receives.
-rw-r--r--django/contrib/auth/management/commands/createsuperuser.py1
-rw-r--r--django/core/management/__init__.py14
-rw-r--r--django/core/management/base.py9
-rw-r--r--django/core/management/commands/flush.py1
-rw-r--r--django/core/management/commands/inspectdb.py3
-rw-r--r--docs/releases/2.0.txt12
-rw-r--r--tests/auth_tests/test_management.py1
-rw-r--r--tests/user_commands/tests.py19
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):
"""