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

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.
parent 4423757c
Loading
Loading
Loading
Loading
+4 −9
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@ from StringIO import StringIO

from django.contrib.auth import models, management
from django.contrib.auth.management.commands import changepassword
from django.core.management.base import CommandError
from django.test import TestCase


@@ -56,16 +57,10 @@ class ChangepasswordManagementCommandTestCase(TestCase):
    def test_that_max_tries_exits_1(self):
        """
        A CommandError should be thrown by handle() if the user enters in
        mismatched passwords three times. This should be caught by execute() and
        converted to a SystemExit
        mismatched passwords three times.
        """
        command = changepassword.Command()
        command._get_pass = lambda *args: args or 'foo'

        self.assertRaises(
            SystemExit,
            command.execute,
            "joe",
            stdout=self.stdout,
            stderr=self.stderr
        )
        with self.assertRaises(CommandError):
            command.execute("joe", stdout=self.stdout, stderr=self.stderr)
+17 −27
Original line number Diff line number Diff line
@@ -97,8 +97,9 @@ class BaseCommand(object):
       output and, if the command is intended to produce a block of
       SQL statements, will be wrapped in ``BEGIN`` and ``COMMIT``.

    4. If ``handle()`` raised a ``CommandError``, ``execute()`` will
       instead print an error message to ``stderr``.
    4. If ``handle()`` or ``execute()`` raised any exception (e.g.
       ``CommandError``), ``run_from_argv()`` will  instead print an error
       message to ``stderr``.

    Thus, the ``handle()`` method is typically the starting point for
    subclasses; many built-in commands and command types either place
@@ -210,23 +211,27 @@ class BaseCommand(object):
    def run_from_argv(self, argv):
        """
        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])
        options, args = parser.parse_args(argv[2:])
        handle_default_options(options)
        try:
            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):
        """
        Try to execute this command, performing model validation if
        needed (as controlled by the attribute
        ``self.requires_model_validation``, except if force-skipped). If the
        command raises a ``CommandError``, intercept it and print it sensibly
        to stderr.
        ``self.requires_model_validation``, except if force-skipped).
        """
        show_traceback = options.get('traceback', False)

        # Switch to English, because django-admin.py creates database content
        # 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)

        if self.can_import_settings:
            try:
            from django.utils import translation
            saved_lang = translation.get_language()
            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:
            if self.requires_model_validation and not options.get('skip_validation'):
@@ -265,12 +261,6 @@ class BaseCommand(object):
                self.stdout.write(output)
                if self.output_transaction:
                    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:
            if saved_lang is not None:
                translation.activate(saved_lang)
+7 −3
Original line number Diff line number Diff line
@@ -317,8 +317,12 @@ Exception class indicating a problem while executing a management
command.

If this exception is raised during the execution of a management
command, it will be caught and turned into a nicely-printed error
message to the appropriate output stream (i.e., stderr); as a
result, raising this exception (with a sensible description of the
command from a command line console, it will be caught and turned into a
nicely-printed error message to the appropriate output stream (i.e., stderr);
as a result, raising this exception (with a sensible description of the
error) is the preferred way to indicate that something has gone
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.
+4 −0
Original line number Diff line number Diff line
@@ -75,6 +75,10 @@ Django 1.5 also includes several smaller improvements worth noting:

* 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
  out-of-memory errors when dumping large datasets.

+2 −2
Original line number Diff line number Diff line
@@ -185,14 +185,14 @@ class FixtureLoadingTests(TestCase):
            exclude_list=['fixtures.Article', 'fixtures.Book', 'sites'])

        # Excluding a bogus app should throw an error
        self.assertRaises(SystemExit,
        self.assertRaises(management.CommandError,
                          self._dumpdata_assert,
                          ['fixtures', 'sites'],
                          '',
                          exclude_list=['foo_app'])

        # Excluding a bogus model should throw an error
        self.assertRaises(SystemExit,
        self.assertRaises(management.CommandError,
                          self._dumpdata_assert,
                          ['fixtures', 'sites'],
                          '',
Loading