Commit 85686386 authored by Claude Paroz's avatar Claude Paroz
Browse files

Fixed #19973 -- Replaced optparse by argparse in management commands

Thanks Tim Graham for the review.
parent 79956d06
Loading
Loading
Loading
Loading
+40 −94
Original line number Diff line number Diff line
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
    else:
        klass = 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
        command = app_name
    else:
            defaults[opt.dest] = opt.default
    defaults.update(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.
        command = load_command_class(app_name, name)

        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)
    # 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:
                    # 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)
        # Legacy optparse method
        defaults, _ = parser.parse_args(args=[])
        defaults = dict(defaults.__dict__, **options)

    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
            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
                        subcommand_cls.option_list]
                            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.
        """
        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 = LaxOptionParser(usage="%prog subcommand [options] [args]",
                                 version=django.get_version(),
                                 option_list=BaseCommand.option_list)
        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_args(self.argv)
            options, args = parser.parse_known_args(self.argv[2:])
            handle_default_options(options)
        except:  # Needed because parser.parse_args can raise SystemExit
        except CommandError:
            pass  # Ignore any option errors at this point.

        try:
            subcommand = self.argv[1]
        except IndexError:
            subcommand = 'help'  # Display help if no arguments were given.

        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)
+101 −30
Original line number Diff line number Diff line
@@ -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,
        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(),
                            option_list=self.option_list)
                                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])

        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 = '<app_label app_label ...>'
    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 = '<label label ...>'
    label = 'label'
    missing_args_message = "Enter at least one %s." % label

    def handle(self, *labels, **options):
        if not labels:
            raise CommandError('Enter at least one %s.' % self.label)
    def add_arguments(self, parser):
        parser.add_argument('args', metavar=self.label, nargs='+')

    def handle(self, *labels, **options):
        output = []
        for label in labels:
            label_output = self.handle_label(label, **options)
+4 −6
Original line number Diff line number Diff line
@@ -50,13 +50,11 @@ class Command(BaseCommand):
        super(Command, self).run_from_argv(argv)

    def create_parser(self, prog_name, subcommand):
        parser = super(Command, self).create_parser(prog_name, subcommand)
        test_runner_class = get_runner(settings, self.test_runner)
        options = self.option_list + getattr(
            test_runner_class, 'option_list', ())
        return OptionParser(prog=prog_name,
                            usage=self.usage(subcommand),
                            version=self.get_version(),
                            option_list=options)
        for opt in getattr(test_runner_class, 'option_list', ()):
            parser.add_option(opt)
        return parser

    def execute(self, *args, **options):
        if int(options['verbosity']) > 0:
+0 −1
Original line number Diff line number Diff line
@@ -4,7 +4,6 @@ from django.core.management.base import AppCommand
class Command(AppCommand):
    help = 'Test Application-based commands'
    requires_system_checks = False
    args = '[app_label ...]'

    def handle_app_config(self, app_config, **options):
        print('EXECUTE:AppCommand name=%s, options=%s' % (app_config.name, sorted(options.items())))
+16 −16

File changed.

Preview size limit exceeded, changes collapsed.

Loading