diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index 3b4ce960c1..23733b7709 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -1,6 +1,7 @@ +from __future__ import unicode_literals + import collections from importlib import import_module -from optparse import OptionParser, NO_DEFAULT import os import sys @@ -8,7 +9,8 @@ import django from django.apps import apps from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.core.management.base import BaseCommand, CommandError, handle_default_options +from django.core.management.base import (BaseCommand, CommandError, + CommandParser, handle_default_options) from django.core.management.color import color_style from django.utils import lru_cache from django.utils import six @@ -93,78 +95,21 @@ def call_command(name, *args, **options): if isinstance(app_name, BaseCommand): # If the command is already loaded, use it directly. - klass = app_name + command = app_name else: - klass = load_command_class(app_name, name) + command = load_command_class(app_name, name) - # Grab out a list of defaults from the options. optparse does this for us - # when the script runs from the command line, but since call_command can - # be called programmatically, we need to simulate the loading and handling - # of defaults (see #10080 for details). - defaults = {} - for opt in klass.option_list: - if opt.default is NO_DEFAULT: - defaults[opt.dest] = None - else: - defaults[opt.dest] = opt.default - defaults.update(options) + # Simulate argument parsing to get the option defaults (see #10080 for details). + parser = command.create_parser('', name) + if command.use_argparse: + defaults = parser.parse_args(args=args) + defaults = dict(defaults._get_kwargs(), **options) + else: + # Legacy optparse method + defaults, _ = parser.parse_args(args=[]) + defaults = dict(defaults.__dict__, **options) - return klass.execute(*args, **defaults) - - -class LaxOptionParser(OptionParser): - """ - An option parser that doesn't raise any errors on unknown options. - - This is needed because the --settings and --pythonpath options affect - the commands (and thus the options) that are available to the user. - """ - def error(self, msg): - pass - - def print_help(self): - """Output nothing. - - The lax options are included in the normal option parser, so under - normal usage, we don't need to print the lax options. - """ - pass - - def print_lax_help(self): - """Output the basic options available to every command. - - This just redirects to the default print_help() behavior. - """ - OptionParser.print_help(self) - - def _process_args(self, largs, rargs, values): - """ - Overrides OptionParser._process_args to exclusively handle default - options and ignore args and other options. - - This overrides the behavior of the super class, which stop parsing - at the first unrecognized option. - """ - while rargs: - arg = rargs[0] - try: - if arg[0:2] == "--" and len(arg) > 2: - # process a single long option (possibly with value(s)) - # the superclass code pops the arg off rargs - self._process_long_opt(rargs, values) - elif arg[:1] == "-" and len(arg) > 1: - # process a cluster of short options (possibly with - # value(s) for the last one only) - # the superclass code pops the arg off rargs - self._process_short_opts(rargs, values) - else: - # it's either a non-default option or an arg - # either way, add it to the args list so we can keep - # dealing with options - del rargs[0] - raise Exception - except: # Needed because we might need to catch a SystemExit - largs.append(arg) + return command.execute(*args, **defaults) class ManagementUtility(object): @@ -296,8 +241,13 @@ class ManagementUtility(object): # Fail silently if DJANGO_SETTINGS_MODULE isn't set. The # user will find out once they execute the command. pass - options += [(s_opt.get_opt_string(), s_opt.nargs) for s_opt in - subcommand_cls.option_list] + parser = subcommand_cls.create_parser('', cwords[0]) + if subcommand_cls.use_argparse: + options += [(sorted(s_opt.option_strings)[0], s_opt.nargs != 0) for s_opt in + parser._actions if s_opt.option_strings] + else: + options += [(s_opt.get_opt_string(), s_opt.nargs) for s_opt in + parser.option_list] # filter out previously specified options from available options prev_opts = [x.split('=')[0] for x in cwords[1:cword - 1]] options = [opt for opt in options if opt[0] not in prev_opts] @@ -317,23 +267,24 @@ class ManagementUtility(object): Given the command-line arguments, this figures out which subcommand is being run, creates a parser appropriate to that command, and runs it. """ - # Preprocess options to extract --settings and --pythonpath. - # These options could affect the commands that are available, so they - # must be processed early. - parser = LaxOptionParser(usage="%prog subcommand [options] [args]", - version=django.get_version(), - option_list=BaseCommand.option_list) - try: - options, args = parser.parse_args(self.argv) - handle_default_options(options) - except: # Needed because parser.parse_args can raise SystemExit - pass # Ignore any option errors at this point. - try: subcommand = self.argv[1] except IndexError: subcommand = 'help' # Display help if no arguments were given. + # 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.add_argument('--settings') + parser.add_argument('--pythonpath') + parser.add_argument('args', nargs='*') # catch-all + try: + options, args = parser.parse_known_args(self.argv[2:]) + handle_default_options(options) + except CommandError: + pass # Ignore any option errors at this point. + no_settings_commands = [ 'help', 'version', '--help', '--version', '-h', 'compilemessages', 'makemessages', @@ -355,22 +306,17 @@ class ManagementUtility(object): self.autocomplete() if subcommand == 'help': - if len(args) <= 2: - parser.print_lax_help() - sys.stdout.write(self.main_help_text() + '\n') - elif args[2] == '--commands': + if '--commands' in args: sys.stdout.write(self.main_help_text(commands_only=True) + '\n') + elif len(options.args) < 1: + sys.stdout.write(self.main_help_text() + '\n') else: - self.fetch_command(args[2]).print_help(self.prog_name, args[2]) - elif subcommand == 'version': - sys.stdout.write(parser.get_version() + '\n') + self.fetch_command(options.args[0]).print_help(self.prog_name, options.args[0]) # Special-cases: We want 'django-admin.py --version' and # 'django-admin.py --help' to work, for backwards compatibility. - elif self.argv[1:] == ['--version']: - # LaxOptionParser already takes care of printing the version. - pass + elif subcommand == 'version' or self.argv[1:] == ['--version']: + sys.stdout.write(django.get_version() + '\n') elif self.argv[1:] in (['--help'], ['-h']): - parser.print_lax_help() sys.stdout.write(self.main_help_text() + '\n') else: self.fetch_command(subcommand).run_from_argv(self.argv) diff --git a/django/core/management/base.py b/django/core/management/base.py index 52f4a074dc..fbffc89786 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -11,13 +11,14 @@ import os import sys import warnings -from optparse import make_option, OptionParser +from argparse import ArgumentParser +from optparse import OptionParser import django from django.core import checks from django.core.exceptions import ImproperlyConfigured from django.core.management.color import color_style, no_style -from django.utils.deprecation import RemovedInDjango19Warning +from django.utils.deprecation import RemovedInDjango19Warning, RemovedInDjango20Warning from django.utils.encoding import force_str @@ -37,6 +38,27 @@ class CommandError(Exception): pass +class CommandParser(ArgumentParser): + """ + Customized ArgumentParser class to improve some error messages and prevent + SystemExit in several occasions, as SystemExit is unacceptable when a + command is called programmatically. + """ + def __init__(self, cmd, **kwargs): + self.cmd = cmd + super(CommandParser, self).__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 + not (args or any([not arg.startswith('-') for arg in args]))): + raise CommandError("Error: %s" % self.cmd.missing_args_message) + return super(CommandParser, self).parse_args(args, namespace) + + def error(self, message): + raise CommandError("Error: %s" % message) + + def handle_default_options(options): """ Include any default options that all commands should accept here @@ -91,7 +113,7 @@ class BaseCommand(object): and calls its ``run_from_argv()`` method. 2. The ``run_from_argv()`` method calls ``create_parser()`` to get - an ``OptionParser`` for the arguments, parses them, performs + an ``ArgumentParser`` for the arguments, parses them, performs any environment changes requested by options like ``pythonpath``, and then calls the ``execute()`` method, passing the parsed arguments. @@ -133,6 +155,7 @@ class BaseCommand(object): ``option_list`` This is the list of ``optparse`` options which will be fed into the command's ``OptionParser`` for parsing arguments. + Deprecated and will be removed in Django 2.0. ``output_transaction`` A boolean indicating whether the command outputs SQL @@ -180,19 +203,7 @@ class BaseCommand(object): settings. This condition will generate a CommandError. """ # Metadata about this command. - option_list = ( - make_option('-v', '--verbosity', action='store', dest='verbosity', default='1', - type='choice', choices=['0', '1', '2', '3'], - help='Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output'), - make_option('--settings', - help='The Python path to a settings module, e.g. "myproject.settings.main". If this isn\'t provided, the DJANGO_SETTINGS_MODULE environment variable will be used.'), - make_option('--pythonpath', - help='A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".'), - make_option('--traceback', action='store_true', - help='Raise on exception'), - make_option('--no-color', action='store_true', dest='no_color', default=False, - help="Don't colorize the command output."), - ) + option_list = () help = '' args = '' @@ -232,6 +243,10 @@ class BaseCommand(object): self.requires_model_validation if has_old_option else True) + @property + def use_argparse(self): + return not bool(self.option_list) + def get_version(self): """ Return the Django version, which should be correct for all @@ -255,14 +270,56 @@ class BaseCommand(object): def create_parser(self, prog_name, subcommand): """ - Create and return the ``OptionParser`` which will be used to + Create and return the ``ArgumentParser`` which will be used to parse the arguments to this command. """ - return OptionParser(prog=prog_name, - usage=self.usage(subcommand), - version=self.get_version(), - option_list=self.option_list) + if not self.use_argparse: + # Backwards compatibility: use deprecated optparse module + warnings.warn("OptionParser usage for Django management commands " + "is deprecated, use ArgumentParser instead", + RemovedInDjango20Warning) + parser = OptionParser(prog=prog_name, + usage=self.usage(subcommand), + version=self.get_version()) + parser.add_option('-v', '--verbosity', action='store', dest='verbosity', default='1', + type='choice', choices=['0', '1', '2', '3'], + help='Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output') + parser.add_option('--settings', + help='The Python path to a settings module, e.g. "myproject.settings.main". If this isn\'t provided, the DJANGO_SETTINGS_MODULE environment variable will be used.') + parser.add_option('--pythonpath', + help='A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".'), + parser.add_option('--traceback', action='store_true', + help='Raise on exception') + parser.add_option('--no-color', action='store_true', dest='no_color', default=False, + help="Don't colorize the command output.") + for opt in self.option_list: + parser.add_option(opt) + else: + parser = CommandParser(self, prog="%s %s" % (prog_name, subcommand), description=self.help or None) + parser.add_argument('--version', action='version', version=self.get_version()) + parser.add_argument('-v', '--verbosity', action='store', dest='verbosity', default='1', + type=int, choices=[0, 1, 2, 3], + help='Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output') + parser.add_argument('--settings', + help='The Python path to a settings module, e.g. "myproject.settings.main". If this isn\'t provided, the DJANGO_SETTINGS_MODULE environment variable will be used.') + parser.add_argument('--pythonpath', + help='A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".') + parser.add_argument('--traceback', action='store_true', + help='Raise on exception') + parser.add_argument('--no-color', action='store_true', dest='no_color', default=False, + help="Don't colorize the command output.") + if self.args: + # Keep compatibility and always accept positional arguments, like optparse when args is set + parser.add_argument('args', nargs='*') + self.add_arguments(parser) + return parser + + def add_arguments(self, parser): + """ + Entry point for subclassed commands to add custom arguments. + """ + pass def print_help(self, prog_name, subcommand): """ @@ -282,10 +339,22 @@ class BaseCommand(object): ``Exception`` is not ``CommandError``, raise it. """ parser = self.create_parser(argv[0], argv[1]) - options, args = parser.parse_args(argv[2:]) + + if self.use_argparse: + options = parser.parse_args(argv[2:]) + cmd_options = vars(options) + # Move positional args out of options to mimic legacy optparse + if 'args' in options: + args = options.args + del cmd_options['args'] + else: + args = () + else: + options, args = parser.parse_args(argv[2:]) + cmd_options = vars(options) handle_default_options(options) try: - self.execute(*args, **options.__dict__) + self.execute(*args, **cmd_options) except Exception as e: if options.traceback or not isinstance(e, CommandError): raise @@ -433,12 +502,14 @@ class AppCommand(BaseCommand): Rather than implementing ``handle()``, subclasses must implement ``handle_app_config()``, which will be called once for each application. """ - args = '' + missing_args_message = "Enter at least one application label." + + def add_arguments(self, parser): + parser.add_argument('args', metavar='app_label', nargs='+', + help='One or more application label.') def handle(self, *app_labels, **options): from django.apps import apps - if not app_labels: - raise CommandError("Enter at least one application label.") try: app_configs = [apps.get_app_config(app_label) for app_label in app_labels] except (LookupError, ImportError) as e: @@ -490,13 +561,13 @@ class LabelCommand(BaseCommand): ``AppCommand`` instead. """ - args = '