From 8e0b6bdcd5b8f9f63a5f370970c71164d25946a7 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 11 Jul 2008 13:18:19 +0000 Subject: [PATCH] Fixed #6017 -- Modified the Lax parser to allow --settings and the other core management arguments to appear anywhere in the argument list. Thanks to Todd O'Bryan for the suggestion and patch. git-svn-id: http://code.djangoproject.com/svn/django/trunk@7888 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/management/__init__.py | 29 ++++++ .../management/commands/base_command.py | 6 ++ tests/regressiontests/admin_scripts/tests.py | 97 ++++++++++++++++++- 3 files changed, 128 insertions(+), 4 deletions(-) diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index 819b19a366..5e01ccbbe9 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -134,6 +134,35 @@ class LaxOptionParser(OptionParser): """ def error(self, msg): pass + + 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 error + except: + largs.append(arg) class ManagementUtility(object): """ diff --git a/tests/regressiontests/admin_scripts/management/commands/base_command.py b/tests/regressiontests/admin_scripts/management/commands/base_command.py index 0187a23b29..438f7038ca 100644 --- a/tests/regressiontests/admin_scripts/management/commands/base_command.py +++ b/tests/regressiontests/admin_scripts/management/commands/base_command.py @@ -1,6 +1,12 @@ from django.core.management.base import BaseCommand +from optparse import make_option class Command(BaseCommand): + option_list = BaseCommand.option_list + ( + make_option('--option_a','-a', action='store', dest='option_a', default='1'), + make_option('--option_b','-b', action='store', dest='option_b', default='2'), + make_option('--option_c','-c', action='store', dest='option_c', default='3'), + ) help = 'Test basic commands' requires_model_validation = False args = '[labels ...]' diff --git a/tests/regressiontests/admin_scripts/tests.py b/tests/regressiontests/admin_scripts/tests.py index cca344bc70..ba7bd8fdaa 100644 --- a/tests/regressiontests/admin_scripts/tests.py +++ b/tests/regressiontests/admin_scripts/tests.py @@ -7,7 +7,7 @@ import os import unittest import shutil -from django import conf, bin +from django import conf, bin, get_version from django.conf import settings class AdminScriptTestCase(unittest.TestCase): @@ -725,26 +725,62 @@ class CommandTypes(AdminScriptTestCase): def tearDown(self): self.remove_settings('settings.py') + def test_version(self): + "--version is handled as a special case" + args = ['--version'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + # Only check the first part of the version number + self.assertOutput(out, get_version().split('-')[0]) + + 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 [options]") + self.assertOutput(err, "Type 'manage.py help ' for help on a specific subcommand.") + + def test_specific_help(self): + "--help can be used on a specific command" + args = ['sqlall','--help'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "Prints the CREATE TABLE, custom SQL and CREATE INDEX SQL statements for the given model module name(s).") + def test_base_command(self): "User BaseCommands can execute when a label is provided" args = ['base_command','testlabel'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('pythonpath', None), ('settings', None), ('traceback', None)]") + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None)]") def test_base_command_no_label(self): "User BaseCommands can execute when no labels are provided" args = ['base_command'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, "EXECUTE:BaseCommand labels=(), options=[('pythonpath', None), ('settings', None), ('traceback', None)]") + self.assertOutput(out, "EXECUTE:BaseCommand labels=(), options=[('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None)]") def test_base_command_multiple_label(self): "User BaseCommands can execute when no labels are provided" args = ['base_command','testlabel','anotherlabel'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel', 'anotherlabel'), options=[('pythonpath', None), ('settings', None), ('traceback', None)]") + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel', 'anotherlabel'), options=[('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None)]") + + def test_base_command_with_option(self): + "User BaseCommands can execute with options when a label is provided" + args = ['base_command','testlabel','--option_a=x'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None)]") + + def test_base_command_with_options(self): + "User BaseCommands can execute with multiple options when a label is provided" + args = ['base_command','testlabel','-a','x','--option_b=y'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', 'y'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None)]") def test_noargs(self): "NoArg Commands can be executed" @@ -815,3 +851,56 @@ class CommandTypes(AdminScriptTestCase): self.assertNoOutput(err) self.assertOutput(out, "EXECUTE:LabelCommand label=testlabel, options=[('pythonpath', None), ('settings', None), ('traceback', None)]") self.assertOutput(out, "EXECUTE:LabelCommand label=anotherlabel, options=[('pythonpath', None), ('settings', None), ('traceback', None)]") + +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 + passed to the command parser, which extracts commands of interest to the + individual command. + """ + def setUp(self): + self.write_settings('settings.py', apps=['django.contrib.auth','django.contrib.contenttypes']) + self.write_settings('alternate_settings.py') + + def tearDown(self): + self.remove_settings('settings.py') + self.remove_settings('alternate_settings.py') + + def test_setting_then_option(self): + "Options passed after settings are correctly handled" + args = ['base_command','testlabel','--settings=alternate_settings','--option_a=x'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None)]") + + def test_setting_then_short_option(self): + "Short options passed after settings are correctly handled" + args = ['base_command','testlabel','--settings=alternate_settings','--option_a=x'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None)]") + + def test_option_then_setting(self): + "Options passed before settings are correctly handled" + args = ['base_command','testlabel','--option_a=x','--settings=alternate_settings'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None)]") + + def test_short_option_then_setting(self): + "Short options passed before settings are correctly handled" + args = ['base_command','testlabel','-a','x','--settings=alternate_settings'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None)]") + + def test_option_then_setting_then_option(self): + "Options are correctly handled when they are passed before and after a setting" + args = ['base_command','testlabel','--option_a=x','--settings=alternate_settings','--option_b=y'] + out, err = self.run_manage(args) + self.assertNoOutput(err) + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', 'y'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None)]") +