Fixed #18387 -- Do not call sys.exit during call_command.

Moved sys.exit(1) so as failing management commands reach it
only when running from command line.
This commit is contained in:
Claude Paroz 2012-05-26 20:50:44 +02:00
parent 4423757c0c
commit f2b6763ad7
7 changed files with 56 additions and 44 deletions

View File

@ -2,6 +2,7 @@ from StringIO import StringIO
from django.contrib.auth import models, management from django.contrib.auth import models, management
from django.contrib.auth.management.commands import changepassword from django.contrib.auth.management.commands import changepassword
from django.core.management.base import CommandError
from django.test import TestCase from django.test import TestCase
@ -56,16 +57,10 @@ class ChangepasswordManagementCommandTestCase(TestCase):
def test_that_max_tries_exits_1(self): def test_that_max_tries_exits_1(self):
""" """
A CommandError should be thrown by handle() if the user enters in A CommandError should be thrown by handle() if the user enters in
mismatched passwords three times. This should be caught by execute() and mismatched passwords three times.
converted to a SystemExit
""" """
command = changepassword.Command() command = changepassword.Command()
command._get_pass = lambda *args: args or 'foo' command._get_pass = lambda *args: args or 'foo'
self.assertRaises( with self.assertRaises(CommandError):
SystemExit, command.execute("joe", stdout=self.stdout, stderr=self.stderr)
command.execute,
"joe",
stdout=self.stdout,
stderr=self.stderr
)

View File

@ -97,8 +97,9 @@ class BaseCommand(object):
output and, if the command is intended to produce a block of output and, if the command is intended to produce a block of
SQL statements, will be wrapped in ``BEGIN`` and ``COMMIT``. SQL statements, will be wrapped in ``BEGIN`` and ``COMMIT``.
4. If ``handle()`` raised a ``CommandError``, ``execute()`` will 4. If ``handle()`` or ``execute()`` raised any exception (e.g.
instead print an error message to ``stderr``. ``CommandError``), ``run_from_argv()`` will instead print an error
message to ``stderr``.
Thus, the ``handle()`` method is typically the starting point for Thus, the ``handle()`` method is typically the starting point for
subclasses; many built-in commands and command types either place subclasses; many built-in commands and command types either place
@ -210,23 +211,27 @@ class BaseCommand(object):
def run_from_argv(self, argv): def run_from_argv(self, argv):
""" """
Set up any environment changes requested (e.g., Python path Set up any environment changes requested (e.g., Python path
and Django settings), then run this command. and Django settings), then run this command. If the
command raises a ``CommandError``, intercept it and print it sensibly
to stderr.
""" """
parser = self.create_parser(argv[0], argv[1]) parser = self.create_parser(argv[0], argv[1])
options, args = parser.parse_args(argv[2:]) options, args = parser.parse_args(argv[2:])
handle_default_options(options) handle_default_options(options)
try:
self.execute(*args, **options.__dict__) self.execute(*args, **options.__dict__)
except Exception as e:
if options.traceback:
self.stderr.write(traceback.format_exc())
self.stderr.write('%s: %s' % (e.__class__.__name__, e))
sys.exit(1)
def execute(self, *args, **options): def execute(self, *args, **options):
""" """
Try to execute this command, performing model validation if Try to execute this command, performing model validation if
needed (as controlled by the attribute needed (as controlled by the attribute
``self.requires_model_validation``, except if force-skipped). If the ``self.requires_model_validation``, except if force-skipped).
command raises a ``CommandError``, intercept it and print it sensibly
to stderr.
""" """
show_traceback = options.get('traceback', False)
# Switch to English, because django-admin.py creates database content # Switch to English, because django-admin.py creates database content
# like permissions, and those shouldn't contain any translations. # like permissions, and those shouldn't contain any translations.
@ -237,18 +242,9 @@ class BaseCommand(object):
self.stderr = OutputWrapper(options.get('stderr', sys.stderr), self.style.ERROR) self.stderr = OutputWrapper(options.get('stderr', sys.stderr), self.style.ERROR)
if self.can_import_settings: if self.can_import_settings:
try:
from django.utils import translation from django.utils import translation
saved_lang = translation.get_language() saved_lang = translation.get_language()
translation.activate('en-us') translation.activate('en-us')
except ImportError as e:
# If settings should be available, but aren't,
# raise the error and quit.
if show_traceback:
traceback.print_exc()
else:
self.stderr.write('Error: %s' % e)
sys.exit(1)
try: try:
if self.requires_model_validation and not options.get('skip_validation'): if self.requires_model_validation and not options.get('skip_validation'):
@ -265,12 +261,6 @@ class BaseCommand(object):
self.stdout.write(output) self.stdout.write(output)
if self.output_transaction: if self.output_transaction:
self.stdout.write('\n' + self.style.SQL_KEYWORD("COMMIT;")) self.stdout.write('\n' + self.style.SQL_KEYWORD("COMMIT;"))
except CommandError as e:
if show_traceback:
traceback.print_exc()
else:
self.stderr.write('Error: %s' % e)
sys.exit(1)
finally: finally:
if saved_lang is not None: if saved_lang is not None:
translation.activate(saved_lang) translation.activate(saved_lang)

View File

@ -317,8 +317,12 @@ Exception class indicating a problem while executing a management
command. command.
If this exception is raised during the execution of a management If this exception is raised during the execution of a management
command, it will be caught and turned into a nicely-printed error command from a command line console, it will be caught and turned into a
message to the appropriate output stream (i.e., stderr); as a nicely-printed error message to the appropriate output stream (i.e., stderr);
result, raising this exception (with a sensible description of the as a result, raising this exception (with a sensible description of the
error) is the preferred way to indicate that something has gone error) is the preferred way to indicate that something has gone
wrong in the execution of a command. wrong in the execution of a command.
If a management command is called from code through
:ref:`call_command <call-command>`, it's up to you to catch the exception
when needed.

View File

@ -75,6 +75,10 @@ Django 1.5 also includes several smaller improvements worth noting:
* The generic views support OPTIONS requests. * The generic views support OPTIONS requests.
* Management commands do not raise ``SystemExit`` any more when called by code
from :ref:`call_command <call-command>`. Any exception raised by the command
(mostly :ref:`CommandError <ref-command-exceptions>`) is propagated.
* The dumpdata management command outputs one row at a time, preventing * The dumpdata management command outputs one row at a time, preventing
out-of-memory errors when dumping large datasets. out-of-memory errors when dumping large datasets.

View File

@ -185,14 +185,14 @@ class FixtureLoadingTests(TestCase):
exclude_list=['fixtures.Article', 'fixtures.Book', 'sites']) exclude_list=['fixtures.Article', 'fixtures.Book', 'sites'])
# Excluding a bogus app should throw an error # Excluding a bogus app should throw an error
self.assertRaises(SystemExit, self.assertRaises(management.CommandError,
self._dumpdata_assert, self._dumpdata_assert,
['fixtures', 'sites'], ['fixtures', 'sites'],
'', '',
exclude_list=['foo_app']) exclude_list=['foo_app'])
# Excluding a bogus model should throw an error # Excluding a bogus model should throw an error
self.assertRaises(SystemExit, self.assertRaises(management.CommandError,
self._dumpdata_assert, self._dumpdata_assert,
['fixtures', 'sites'], ['fixtures', 'sites'],
'', '',

View File

@ -1,6 +1,6 @@
from optparse import make_option from optparse import make_option
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand, CommandError
class Command(BaseCommand): class Command(BaseCommand):
@ -8,11 +8,13 @@ class Command(BaseCommand):
args = '' args = ''
requires_model_validation = True requires_model_validation = True
option_list =[ option_list = BaseCommand.option_list + (
make_option("-s", "--style", default="Rock'n'Roll"), make_option("-s", "--style", default="Rock'n'Roll"),
make_option("-x", "--example") make_option("-x", "--example")
] )
def handle(self, *args, **options): def handle(self, *args, **options):
example = options["example"] example = options["example"]
if example == "raise":
raise CommandError()
self.stdout.write("I don't feel like dancing %s." % options["style"]) self.stdout.write("I don't feel like dancing %s." % options["style"])

View File

@ -1,3 +1,4 @@
import sys
from StringIO import StringIO from StringIO import StringIO
from django.core import management from django.core import management
@ -26,4 +27,20 @@ class CommandTests(TestCase):
self.assertEqual(translation.get_language(), 'fr') self.assertEqual(translation.get_language(), 'fr')
def test_explode(self): def test_explode(self):
""" Test that an unknown command raises CommandError """
self.assertRaises(CommandError, management.call_command, ('explode',)) self.assertRaises(CommandError, management.call_command, ('explode',))
def test_system_exit(self):
""" Exception raised in a command should raise CommandError with
call_command, but SystemExit when run from command line
"""
with self.assertRaises(CommandError):
management.call_command('dance', example="raise")
old_stderr = sys.stderr
sys.stderr = err = StringIO()
try:
with self.assertRaises(SystemExit):
management.ManagementUtility(['manage.py', 'dance', '--example=raise']).execute()
finally:
sys.stderr = old_stderr
self.assertIn("CommandError", err.getvalue())