Explorar o código

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

Thanks Tim Graham for the review.
Claude Paroz %!s(int64=11) %!d(string=hai) anos
pai
achega
8568638603

+ 40 - 94
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)
-
-    # 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)
-
-    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
+        command = load_command_class(app_name, name)
 
-    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.
+    # 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)
 
-        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.
         """
+        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
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 = '<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
django/core/management/commands/test.py

@@ -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
tests/admin_scripts/management/commands/app_command.py

@@ -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
tests/admin_scripts/tests.py

@@ -930,21 +930,21 @@ class ManageAlternateSettings(AdminScriptTestCase):
         "alternate: manage.py can execute user commands if settings are provided as argument"
         args = ['noargs_command', '--settings=alternate_settings']
         out, err = self.run_manage(args)
-        self.assertOutput(out, str_prefix("EXECUTE:NoArgsCommand options=[('no_color', False), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', %(_)s'1')]"))
+        self.assertOutput(out, "EXECUTE:NoArgsCommand options=[('no_color', False), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', False), ('verbosity', 1)]")
         self.assertNoOutput(err)
 
     def test_custom_command_with_environment(self):
         "alternate: manage.py can execute user commands if settings are provided in environment"
         args = ['noargs_command']
         out, err = self.run_manage(args, 'alternate_settings')
-        self.assertOutput(out, str_prefix("EXECUTE:NoArgsCommand options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]"))
+        self.assertOutput(out, "EXECUTE:NoArgsCommand options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', False), ('verbosity', 1)]")
         self.assertNoOutput(err)
 
     def test_custom_command_output_color(self):
         "alternate: manage.py output syntax color can be deactivated with the `--no-color` option"
         args = ['noargs_command', '--no-color', '--settings=alternate_settings']
         out, err = self.run_manage(args)
-        self.assertOutput(out, str_prefix("EXECUTE:NoArgsCommand options=[('no_color', True), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', %(_)s'1')]"))
+        self.assertOutput(out, "EXECUTE:NoArgsCommand options=[('no_color', True), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', False), ('verbosity', 1)]")
         self.assertNoOutput(err)
 
 
@@ -1340,13 +1340,13 @@ class CommandTypes(AdminScriptTestCase):
     def test_version_alternative(self):
         "--version is equivalent to version"
         args1, args2 = ['version'], ['--version']
-        self.assertEqual(self.run_manage(args1), self.run_manage(args2))
+        # It's possible one outputs on stderr and the other on stdout, hence the set
+        self.assertEqual(set(self.run_manage(args1)), set(self.run_manage(args2)))
 
     def test_help(self):
         "help is handled as a special case"
         args = ['help']
         out, err = self.run_manage(args)
-        self.assertOutput(out, "Usage: manage.py subcommand [options] [args]")
         self.assertOutput(out, "Type 'manage.py help <subcommand>' for help on a specific subcommand.")
         self.assertOutput(out, '[django]')
         self.assertOutput(out, 'startapp')
@@ -1356,7 +1356,7 @@ class CommandTypes(AdminScriptTestCase):
         "help --commands shows the list of all available commands"
         args = ['help', '--commands']
         out, err = self.run_manage(args)
-        self.assertNotInOutput(out, 'Usage:')
+        self.assertNotInOutput(out, 'usage:')
         self.assertNotInOutput(out, 'Options:')
         self.assertNotInOutput(out, '[django]')
         self.assertOutput(out, 'startapp')
@@ -1489,13 +1489,13 @@ class CommandTypes(AdminScriptTestCase):
         args = ['noargs_command']
         out, err = self.run_manage(args)
         self.assertNoOutput(err)
-        self.assertOutput(out, str_prefix("EXECUTE:NoArgsCommand options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]"))
+        self.assertOutput(out, "EXECUTE:NoArgsCommand options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', False), ('verbosity', 1)]")
 
     def test_noargs_with_args(self):
         "NoArg Commands raise an error if an argument is provided"
         args = ['noargs_command', 'argument']
         out, err = self.run_manage(args)
-        self.assertOutput(err, "Error: Command doesn't accept any arguments")
+        self.assertOutput(err, "Error: unrecognized arguments: argument")
 
     def test_app_command(self):
         "User AppCommands can execute when a single app name is provided"
@@ -1503,7 +1503,7 @@ class CommandTypes(AdminScriptTestCase):
         out, err = self.run_manage(args)
         self.assertNoOutput(err)
         self.assertOutput(out, "EXECUTE:AppCommand name=django.contrib.auth, options=")
-        self.assertOutput(out, str_prefix(", options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]"))
+        self.assertOutput(out, ", options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', False), ('verbosity', 1)]")
 
     def test_app_command_no_apps(self):
         "User AppCommands raise an error when no app name is provided"
@@ -1517,9 +1517,9 @@ class CommandTypes(AdminScriptTestCase):
         out, err = self.run_manage(args)
         self.assertNoOutput(err)
         self.assertOutput(out, "EXECUTE:AppCommand name=django.contrib.auth, options=")
-        self.assertOutput(out, str_prefix(", options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]"))
+        self.assertOutput(out, ", options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', False), ('verbosity', 1)]")
         self.assertOutput(out, "EXECUTE:AppCommand name=django.contrib.contenttypes, options=")
-        self.assertOutput(out, str_prefix(", options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]"))
+        self.assertOutput(out, ", options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', False), ('verbosity', 1)]")
 
     def test_app_command_invalid_app_label(self):
         "User AppCommands can execute when a single app name is provided"
@@ -1538,7 +1538,7 @@ class CommandTypes(AdminScriptTestCase):
         args = ['label_command', 'testlabel']
         out, err = self.run_manage(args)
         self.assertNoOutput(err)
-        self.assertOutput(out, str_prefix("EXECUTE:LabelCommand label=testlabel, options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]"))
+        self.assertOutput(out, "EXECUTE:LabelCommand label=testlabel, options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', False), ('verbosity', 1)]")
 
     def test_label_command_no_label(self):
         "User LabelCommands raise an error if no label is provided"
@@ -1551,8 +1551,8 @@ class CommandTypes(AdminScriptTestCase):
         args = ['label_command', 'testlabel', 'anotherlabel']
         out, err = self.run_manage(args)
         self.assertNoOutput(err)
-        self.assertOutput(out, str_prefix("EXECUTE:LabelCommand label=testlabel, options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]"))
-        self.assertOutput(out, str_prefix("EXECUTE:LabelCommand label=anotherlabel, options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', %(_)s'1')]"))
+        self.assertOutput(out, "EXECUTE:LabelCommand label=testlabel, options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', False), ('verbosity', 1)]")
+        self.assertOutput(out, "EXECUTE:LabelCommand label=anotherlabel, options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', False), ('verbosity', 1)]")
 
     def test_requires_model_validation_and_requires_system_checks_both_defined(self):
         with warnings.catch_warnings(record=True):
@@ -1587,8 +1587,8 @@ class ArgumentOrder(AdminScriptTestCase):
     """Tests for 2-stage argument parsing scheme.
 
     django-admin command arguments are parsed in 2 parts; the core arguments
-    (--settings, --traceback and --pythonpath) are parsed using a Lax parser.
-    This Lax parser ignores any unknown options. Then the full settings are
+    (--settings, --traceback and --pythonpath) are parsed using a basic parser,
+    ignoring any unknown options. Then the full settings are
     passed to the command parser, which extracts commands of interest to the
     individual command.
     """

+ 2 - 2
tests/bash_completion/tests.py

@@ -46,13 +46,13 @@ class BashCompletionTests(unittest.TestCase):
 
     def test_django_admin_py(self):
         "django_admin.py will autocomplete option flags"
-        self._user_input('django-admin.py sqlall --v')
+        self._user_input('django-admin.py sqlall --verb')
         output = self._run_autocomplete()
         self.assertEqual(output, ['--verbosity='])
 
     def test_manage_py(self):
         "manage.py will autocomplete option flags"
-        self._user_input('manage.py sqlall --v')
+        self._user_input('manage.py sqlall --verb')
         output = self._run_autocomplete()
         self.assertEqual(output, ['--verbosity='])
 

+ 19 - 0
tests/user_commands/management/commands/optparse_cmd.py

@@ -0,0 +1,19 @@
+from optparse import make_option
+
+from django.core.management.base import BaseCommand, CommandError
+
+
+class Command(BaseCommand):
+    help = "Test optparse compatibility."
+    args = ''
+
+    option_list = BaseCommand.option_list + (
+        make_option("-s", "--style", default="Rock'n'Roll"),
+        make_option("-x", "--example")
+    )
+
+    def handle(self, *args, **options):
+        example = options["example"]
+        # BaseCommand default option is available
+        options['verbosity']
+        self.stdout.write("All right, let's dance %s." % options["style"])

+ 18 - 0
tests/user_commands/tests.py

@@ -74,6 +74,24 @@ class CommandTests(SimpleTestCase):
             if current_path is not None:
                 os.environ['PATH'] = current_path
 
+    def test_optparse_compatibility(self):
+        """
+        optparse should be supported during Django 1.8/1.9 releases.
+        """
+        out = StringIO()
+        management.call_command('optparse_cmd', stdout=out)
+        self.assertEqual(out.getvalue(), "All right, let's dance Rock'n'Roll.\n")
+
+        # Simulate command line execution
+        old_stdout, old_stderr = sys.stdout, sys.stderr
+        sys.stdout, sys.stderr = StringIO(), StringIO()
+        try:
+            management.execute_from_command_line(['django-admin', 'optparse_cmd'])
+        finally:
+            output = sys.stdout.getvalue()
+            sys.stdout, sys.stderr = old_stdout, old_stderr
+        self.assertEqual(output, "All right, let's dance Rock'n'Roll.\n")
+
 
 class UtilsTests(SimpleTestCase):