From c9ebe4ca4e3f5d5d76bfbdae489e3f44e32416e5 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Fri, 22 Oct 2021 16:38:14 +0200 Subject: [PATCH] [4.0.x] Fixed #33205 -- Made call_command() raise TypeError when dest with multiple arguments is passed. Backport of c1e4111c74ee9d9f48cbee5a5b7c40289203c93d from main --- django/core/management/__init__.py | 6 ++++ ...ually_exclusive_required_with_same_dest.py | 13 +++++++ tests/user_commands/tests.py | 35 +++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 tests/user_commands/management/commands/mutually_exclusive_required_with_same_dest.py diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index 4e30a28f338..049297b5aa6 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -149,6 +149,12 @@ def call_command(command_name, *args, **options): opt.dest in options and (opt.required or opt in mutually_exclusive_required_options) ): + opt_dest_count = sum(v == opt.dest for v in opt_mapping.values()) + if opt_dest_count > 1: + raise TypeError( + f'Cannot pass the dest {opt.dest!r} that matches multiple ' + f'arguments via **options.' + ) parse_args.append(min(opt.option_strings)) if isinstance(opt, (_AppendConstAction, _CountAction, _StoreConstAction)): continue diff --git a/tests/user_commands/management/commands/mutually_exclusive_required_with_same_dest.py b/tests/user_commands/management/commands/mutually_exclusive_required_with_same_dest.py new file mode 100644 index 00000000000..1a9ab5576db --- /dev/null +++ b/tests/user_commands/management/commands/mutually_exclusive_required_with_same_dest.py @@ -0,0 +1,13 @@ +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + def add_arguments(self, parser): + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--for', dest='until', action='store') + group.add_argument('--until', action='store') + + def handle(self, *args, **options): + for option, value in options.items(): + if value is not None: + self.stdout.write('%s=%s' % (option, value)) diff --git a/tests/user_commands/tests.py b/tests/user_commands/tests.py index 60cfe86b3d8..a727e56efb3 100644 --- a/tests/user_commands/tests.py +++ b/tests/user_commands/tests.py @@ -275,6 +275,41 @@ class CommandTests(SimpleTestCase): ) self.assertIn(expected_output, out.getvalue()) + def test_mutually_exclusive_group_required_with_same_dest_options(self): + tests = [ + {'until': '2'}, + {'for': '1', 'until': '2'}, + ] + msg = ( + "Cannot pass the dest 'until' that matches multiple arguments via " + "**options." + ) + for options in tests: + with self.subTest(options=options): + with self.assertRaisesMessage(TypeError, msg): + management.call_command( + 'mutually_exclusive_required_with_same_dest', + **options, + ) + + def test_mutually_exclusive_group_required_with_same_dest_args(self): + tests = [ + ('--until=1',), + ('--until', 1), + ('--for=1',), + ('--for', 1), + ] + for args in tests: + out = StringIO() + with self.subTest(options=args): + management.call_command( + 'mutually_exclusive_required_with_same_dest', + *args, + stdout=out, + ) + output = out.getvalue() + self.assertIn('until=1', output) + def test_required_list_option(self): tests = [ (('--foo-list', [1, 2]), {}),