From 2b03e8e9e8205ae3a3aa128764277e70b7c30803 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Wed, 26 Jun 2019 22:04:58 +0200 Subject: [PATCH] Fixed #30584 -- Fixed management command when using subparsers with dest parameter. --- django/core/management/__init__.py | 16 ++++++++++-- .../management/commands/subparser_dest.py | 13 ++++++++++ .../management/commands/subparser_required.py | 13 ++++++++++ tests/user_commands/tests.py | 25 +++++++++++++++++++ 4 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 tests/user_commands/management/commands/subparser_dest.py create mode 100644 tests/user_commands/management/commands/subparser_required.py diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index b8f47fa4a4..5f2e64761e 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -2,6 +2,7 @@ import functools import os import pkgutil import sys +from argparse import _SubParsersAction from collections import defaultdict from difflib import get_close_matches from importlib import import_module @@ -118,17 +119,28 @@ def call_command(command_name, *args, **options): } arg_options = {opt_mapping.get(key, key): value for key, value in options.items()} parse_args = [str(a) for a in args] + + def get_actions(parser): + # Parser actions and actions from sub-parser choices. + for opt in parser._actions: + if isinstance(opt, _SubParsersAction): + for sub_opt in opt.choices.values(): + yield from get_actions(sub_opt) + else: + yield opt + + parser_actions = list(get_actions(parser)) # Any required arguments which are passed in via **options must be passed # to parse_args(). parse_args += [ '{}={}'.format(min(opt.option_strings), arg_options[opt.dest]) - for opt in parser._actions if opt.required and opt.dest in options + for opt in parser_actions if opt.required and opt.dest in options ] defaults = parser.parse_args(args=parse_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} + dest_parameters = {action.dest for action in parser_actions} valid_options = (dest_parameters | stealth_options).union(opt_mapping) unknown_options = set(options) - valid_options if unknown_options: diff --git a/tests/user_commands/management/commands/subparser_dest.py b/tests/user_commands/management/commands/subparser_dest.py new file mode 100644 index 0000000000..ffea7efac7 --- /dev/null +++ b/tests/user_commands/management/commands/subparser_dest.py @@ -0,0 +1,13 @@ +from django.core.management.base import BaseCommand +from django.utils.version import PY37 + + +class Command(BaseCommand): + def add_arguments(self, parser): + kwargs = {'required': True} if PY37 else {} + subparsers = parser.add_subparsers(dest='subcommand', **kwargs) + parser_foo = subparsers.add_parser('foo') + parser_foo.add_argument('--bar') + + def handle(self, *args, **options): + self.stdout.write(','.join(options)) diff --git a/tests/user_commands/management/commands/subparser_required.py b/tests/user_commands/management/commands/subparser_required.py new file mode 100644 index 0000000000..995b0dce66 --- /dev/null +++ b/tests/user_commands/management/commands/subparser_required.py @@ -0,0 +1,13 @@ +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + def add_arguments(self, parser): + subparsers_1 = parser.add_subparsers(dest='subcommand_1') + parser_foo_1 = subparsers_1.add_parser('foo_1') + subparsers_2 = parser_foo_1.add_subparsers(dest='subcommand_2') + parser_foo_2 = subparsers_2.add_parser('foo_2') + parser_foo_2.add_argument('--bar', required=True) + + def handle(self, *args, **options): + self.stdout.write(','.join(options)) diff --git a/tests/user_commands/tests.py b/tests/user_commands/tests.py index 76991b1122..a53c781ac6 100644 --- a/tests/user_commands/tests.py +++ b/tests/user_commands/tests.py @@ -15,6 +15,7 @@ from django.db import connection from django.test import SimpleTestCase, override_settings from django.test.utils import captured_stderr, extend_sys_path from django.utils import translation +from django.utils.version import PY37 from .management.commands import dance @@ -218,10 +219,34 @@ class CommandTests(SimpleTestCase): management.call_command('subparser', 'foo', 12, stdout=out) self.assertIn('bar', out.getvalue()) + def test_subparser_dest_args(self): + out = StringIO() + management.call_command('subparser_dest', 'foo', bar=12, stdout=out) + self.assertIn('bar', out.getvalue()) + + def test_subparser_dest_required_args(self): + out = StringIO() + management.call_command('subparser_required', 'foo_1', 'foo_2', bar=12, stdout=out) + self.assertIn('bar', out.getvalue()) + def test_subparser_invalid_option(self): msg = "Error: invalid choice: 'test' (choose from 'foo')" with self.assertRaisesMessage(CommandError, msg): management.call_command('subparser', 'test', 12) + if PY37: + # "required" option requires Python 3.7 and later. + msg = 'Error: the following arguments are required: subcommand' + with self.assertRaisesMessage(CommandError, msg): + management.call_command('subparser_dest', subcommand='foo', bar=12) + else: + msg = ( + 'Unknown option(s) for subparser_dest command: subcommand. ' + 'Valid options are: bar, force_color, help, no_color, ' + 'pythonpath, settings, skip_checks, stderr, stdout, ' + 'traceback, verbosity, version.' + ) + with self.assertRaisesMessage(TypeError, msg): + management.call_command('subparser_dest', subcommand='foo', bar=12) def test_create_parser_kwargs(self): """BaseCommand.create_parser() passes kwargs to CommandParser."""