diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index 75cd48461c..d74befa844 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -102,8 +102,12 @@ def call_command(name, *args, **options): # Simulate argument parsing to get the option defaults (see #10080 for details). parser = command.create_parser('', name) if command.use_argparse: + # Use the `dest` option name from the parser option + opt_mapping = dict((sorted(s_opt.option_strings)[0].lstrip('-').replace('-', '_'), s_opt.dest) + for s_opt in parser._actions if s_opt.option_strings) + arg_options = dict((opt_mapping.get(key, key), value) for key, value in options.items()) defaults = parser.parse_args(args=args) - defaults = dict(defaults._get_kwargs(), **options) + defaults = dict(defaults._get_kwargs(), **arg_options) else: # Legacy optparse method defaults, _ = parser.parse_args(args=[]) diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index d271eafc00..85d0ab06fe 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -1824,10 +1824,27 @@ Examples:: management.call_command('loaddata', 'test_data', verbosity=0) Note that command options that take no arguments are passed as keywords -with ``True`` or ``False``:: +with ``True`` or ``False``, as you can see with the ``interactive`` option above. +Named arguments can be passed by using either one of the following syntaxes:: + + # Similar to the command line + management.call_command('dumpdata', '--natural') + + # Named argument similar to the command line minus the initial dashes and + # with internal dashes replaced by underscores + management.call_command('dumpdata', natural=True) + + # `use_natural_keys` is the option destination variable management.call_command('dumpdata', use_natural_keys=True) +.. versionchanged:: 1.8 + + The first syntax is now supported thanks to management commands using the + :py:mod:`argparse` module. For the second syntax, Django previously passed + the option name as-is to the command, now it is always using the ``dest`` + variable name (which may or may not be the same as the option name). + Command options which take multiple options are passed a list:: management.call_command('dumpdata', exclude=['contenttypes', 'auth']) diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 318b6ce49f..86d39c4f88 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -196,6 +196,13 @@ Management Commands * :djadmin:`inspectdb` now outputs ``Meta.unique_together``. +* When calling management commands from code through :ref:`call_command + ` and passing options, the option name can match the command + line option name (without the initial dashes) or the final option destination + variable name, but in either case, the resulting option received by the + command is now always the ``dest`` name specified in the command option + definition (as long as the command uses the new :py:mod:`argparse` module). + Models ^^^^^^ diff --git a/tests/user_commands/management/commands/dance.py b/tests/user_commands/management/commands/dance.py index 0c47401c25..e27042f9d4 100644 --- a/tests/user_commands/management/commands/dance.py +++ b/tests/user_commands/management/commands/dance.py @@ -9,9 +9,11 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("-s", "--style", default="Rock'n'Roll") parser.add_argument("-x", "--example") + parser.add_argument("--opt-3", action='store_true', dest='option3') def handle(self, *args, **options): example = options["example"] if example == "raise": raise CommandError() self.stdout.write("I don't feel like dancing %s." % options["style"]) + self.stdout.write(','.join(options.keys())) diff --git a/tests/user_commands/tests.py b/tests/user_commands/tests.py index ac2e51c807..398e01eb6b 100644 --- a/tests/user_commands/tests.py +++ b/tests/user_commands/tests.py @@ -15,14 +15,15 @@ class CommandTests(SimpleTestCase): def test_command(self): out = StringIO() management.call_command('dance', stdout=out) - self.assertEqual(out.getvalue(), - "I don't feel like dancing Rock'n'Roll.\n") + self.assertIn("I don't feel like dancing Rock'n'Roll.\n", out.getvalue()) def test_command_style(self): out = StringIO() management.call_command('dance', style='Jive', stdout=out) - self.assertEqual(out.getvalue(), - "I don't feel like dancing Jive.\n") + self.assertIn("I don't feel like dancing Jive.\n", out.getvalue()) + # Passing options as arguments also works (thanks argparse) + management.call_command('dance', '--style', 'Jive', stdout=out) + self.assertIn("I don't feel like dancing Jive.\n", out.getvalue()) def test_language_preserved(self): out = StringIO() @@ -76,6 +77,17 @@ class CommandTests(SimpleTestCase): if current_path is not None: os.environ['PATH'] = current_path + def test_call_command_option_parsing(self): + """ + When passing the long option name to call_command, the available option + key is the option dest name (#22985). + """ + out = StringIO() + management.call_command('dance', stdout=out, opt_3=True) + self.assertIn("option3", out.getvalue()) + self.assertNotIn("opt_3", out.getvalue()) + self.assertNotIn("opt-3", out.getvalue()) + def test_optparse_compatibility(self): """ optparse should be supported during Django 1.8/1.9 releases.