diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index 688340e49d..8d39b86472 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -311,7 +311,7 @@ class ManagementUtility: # Preprocess options to extract --settings and --pythonpath. # These options could affect the commands that are available, so they # must be processed early. - parser = CommandParser(None, usage="%(prog)s subcommand [options] [args]", add_help=False) + parser = CommandParser(usage='%(prog)s subcommand [options] [args]', add_help=False) parser.add_argument('--settings') parser.add_argument('--pythonpath') parser.add_argument('args', nargs='*') # catch-all diff --git a/django/core/management/base.py b/django/core/management/base.py index 9461f8654a..45e97c7398 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -42,19 +42,20 @@ class CommandParser(ArgumentParser): SystemExit in several occasions, as SystemExit is unacceptable when a command is called programmatically. """ - def __init__(self, cmd, **kwargs): - self.cmd = cmd + def __init__(self, **kwargs): + self.missing_args_message = kwargs.pop('missing_args_message', None) + self.called_from_command_line = kwargs.pop('called_from_command_line', None) super().__init__(**kwargs) def parse_args(self, args=None, namespace=None): # Catch missing argument for a better error message - if (hasattr(self.cmd, 'missing_args_message') and + if (self.missing_args_message and not (args or any(not arg.startswith('-') for arg in args))): - self.error(self.cmd.missing_args_message) + self.error(self.missing_args_message) return super().parse_args(args, namespace) def error(self, message): - if self.cmd._called_from_command_line: + if self.called_from_command_line: super().error(message) else: raise CommandError("Error: %s" % message) @@ -225,8 +226,10 @@ class BaseCommand: parse the arguments to this command. """ parser = CommandParser( - self, prog="%s %s" % (os.path.basename(prog_name), subcommand), + prog='%s %s' % (os.path.basename(prog_name), subcommand), description=self.help or None, + missing_args_message=getattr(self, 'missing_args_message', None), + called_from_command_line=getattr(self, '_called_from_command_line', None), ) # Add command-specific arguments first so that they appear in the # --help output before arguments common to all commands. diff --git a/tests/user_commands/management/commands/subparser.py b/tests/user_commands/management/commands/subparser.py new file mode 100644 index 0000000000..d3006bd3e8 --- /dev/null +++ b/tests/user_commands/management/commands/subparser.py @@ -0,0 +1,12 @@ +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + + def add_arguments(self, parser): + subparsers = parser.add_subparsers() + parser_foo = subparsers.add_parser('foo') + parser_foo.add_argument('bar', type=int) + + 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 ae05bcfe25..61e649c75a 100644 --- a/tests/user_commands/tests.py +++ b/tests/user_commands/tests.py @@ -206,6 +206,16 @@ class CommandTests(SimpleTestCase): self.assertIn('need_me', out.getvalue()) self.assertIn('needme2', out.getvalue()) + def test_subparser(self): + out = StringIO() + management.call_command('subparser', 'foo', 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) + class CommandRunTests(AdminScriptTestCase): """