From f06beea92999407cc8dad3c47f006b7c727095a6 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Thu, 29 Oct 2020 19:30:43 +0100 Subject: [PATCH] Fixed #32153 -- Fixed management commands when using required list options. Thanks Mark Gajdosik for the report and initial patch. --- django/core/management/__init__.py | 25 +++++++++++++------ .../commands/mutually_exclusive_required.py | 1 + .../commands/required_list_option.py | 10 ++++++++ tests/user_commands/tests.py | 21 ++++++++++++++-- 4 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 tests/user_commands/management/commands/required_list_option.py diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index aff9be4e85c..1ba093e51f8 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -120,7 +120,12 @@ def call_command(command_name, *args, **options): for s_opt in parser._actions if s_opt.option_strings } arg_options = {opt_mapping.get(key, key): value for key, value in options.items()} - parse_args = [str(a) for a in args] + parse_args = [] + for arg in args: + if isinstance(arg, (list, tuple)): + parse_args += map(str, arg) + else: + parse_args.append(str(arg)) def get_actions(parser): # Parser actions and actions from sub-parser choices. @@ -139,15 +144,19 @@ def call_command(command_name, *args, **options): } # Any required arguments which are passed in via **options must be passed # to parse_args(). - parse_args += [ - min(opt.option_strings) - if isinstance(opt, (_AppendConstAction, _CountAction, _StoreConstAction)) - else '{}={}'.format(min(opt.option_strings), arg_options[opt.dest]) - for opt in parser_actions if ( + for opt in parser_actions: + if ( opt.dest in options and (opt.required or opt in mutually_exclusive_required_options) - ) - ] + ): + parse_args.append(min(opt.option_strings)) + if isinstance(opt, (_AppendConstAction, _CountAction, _StoreConstAction)): + continue + value = arg_options[opt.dest] + if isinstance(value, (list, tuple)): + parse_args += map(str, value) + else: + parse_args.append(str(value)) defaults = parser.parse_args(args=parse_args) defaults = dict(defaults._get_kwargs(), **arg_options) # Raise an error if any unknown options were passed. diff --git a/tests/user_commands/management/commands/mutually_exclusive_required.py b/tests/user_commands/management/commands/mutually_exclusive_required.py index 3fbf514c4d0..8e48f126587 100644 --- a/tests/user_commands/management/commands/mutually_exclusive_required.py +++ b/tests/user_commands/management/commands/mutually_exclusive_required.py @@ -7,6 +7,7 @@ class Command(BaseCommand): group = parser.add_mutually_exclusive_group(required=True) group.add_argument('--foo-id', type=int, nargs='?', default=None) group.add_argument('--foo-name', type=str, nargs='?', default=None) + group.add_argument('--foo-list', type=int, nargs='+') group.add_argument('--append_const', action='append_const', const=42) group.add_argument('--const', action='store_const', const=31) group.add_argument('--count', action='count') diff --git a/tests/user_commands/management/commands/required_list_option.py b/tests/user_commands/management/commands/required_list_option.py new file mode 100644 index 00000000000..84f39f0142a --- /dev/null +++ b/tests/user_commands/management/commands/required_list_option.py @@ -0,0 +1,10 @@ +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + def add_arguments(self, parser): + parser.add_argument('--foo-list', nargs='+', type=int, required=True) + + def handle(self, *args, **options): + for option, value in options.items(): + self.stdout.write('%s=%s' % (option, value)) diff --git a/tests/user_commands/tests.py b/tests/user_commands/tests.py index eb2d38fbfbf..9262e2717ac 100644 --- a/tests/user_commands/tests.py +++ b/tests/user_commands/tests.py @@ -244,8 +244,9 @@ class CommandTests(SimpleTestCase): management.call_command('mutually_exclusive_required', foo_name='foo', stdout=out) self.assertIn('foo_name', out.getvalue()) msg = ( - 'Error: one of the arguments --foo-id --foo-name --append_const ' - '--const --count --flag_false --flag_true is required' + 'Error: one of the arguments --foo-id --foo-name --foo-list ' + '--append_const --const --count --flag_false --flag_true is ' + 'required' ) with self.assertRaisesMessage(CommandError, msg): management.call_command('mutually_exclusive_required', stdout=out) @@ -275,6 +276,22 @@ class CommandTests(SimpleTestCase): ) self.assertIn(expected_output, out.getvalue()) + def test_required_list_option(self): + tests = [ + (('--foo-list', [1, 2]), {}), + ((), {'foo_list': [1, 2]}), + ] + for command in ['mutually_exclusive_required', 'required_list_option']: + for args, kwargs in tests: + with self.subTest(command=command, args=args, kwargs=kwargs): + out = StringIO() + management.call_command( + command, + *args, + **{**kwargs, 'stdout': out}, + ) + self.assertIn('foo_list=[1, 2]', out.getvalue()) + def test_required_const_options(self): args = { 'append_const': [42],