commit b8b068ccaab7b3976e1e788daeb75ab6acff3e9b
Author: Claude Paroz <claude@2xlibre.net>
Date: Sat May 26 20:50:44 2012 +0200
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.
diff --git a/django/contrib/auth/tests/management.py b/django/contrib/auth/tests/management.py
index 5d31bd7..81ab0aa 100644
a
|
b
|
from StringIO import StringIO
|
2 | 2 | |
3 | 3 | from django.contrib.auth import models, management |
4 | 4 | from django.contrib.auth.management.commands import changepassword |
| 5 | from django.core.management.base import CommandError |
5 | 6 | from django.test import TestCase |
6 | 7 | |
7 | 8 | |
… |
… |
class ChangepasswordManagementCommandTestCase(TestCase):
|
56 | 57 | def test_that_max_tries_exits_1(self): |
57 | 58 | """ |
58 | 59 | A CommandError should be thrown by handle() if the user enters in |
59 | | mismatched passwords three times. This should be caught by execute() and |
60 | | converted to a SystemExit |
| 60 | mismatched passwords three times. |
61 | 61 | """ |
62 | 62 | command = changepassword.Command() |
63 | 63 | command._get_pass = lambda *args: args or 'foo' |
64 | 64 | |
65 | | self.assertRaises( |
66 | | SystemExit, |
67 | | command.execute, |
68 | | "joe", |
69 | | stdout=self.stdout, |
70 | | stderr=self.stderr |
71 | | ) |
| 65 | with self.assertRaises(CommandError): |
| 66 | command.execute("joe", stdout=self.stdout, stderr=self.stderr) |
diff --git a/django/core/management/base.py b/django/core/management/base.py
index 6e06991..a204f6f 100644
a
|
b
|
class BaseCommand(object):
|
97 | 97 | output and, if the command is intended to produce a block of |
98 | 98 | SQL statements, will be wrapped in ``BEGIN`` and ``COMMIT``. |
99 | 99 | |
100 | | 4. If ``handle()`` raised a ``CommandError``, ``execute()`` will |
101 | | instead print an error message to ``stderr``. |
| 100 | 4. If ``handle()`` or ``execute()`` raised any exception (e.g. |
| 101 | ``CommandError``), ``run_from_argv()`` will instead print an error |
| 102 | message to ``stderr``. |
102 | 103 | |
103 | 104 | Thus, the ``handle()`` method is typically the starting point for |
104 | 105 | subclasses; many built-in commands and command types either place |
… |
… |
class BaseCommand(object):
|
210 | 211 | def run_from_argv(self, argv): |
211 | 212 | """ |
212 | 213 | Set up any environment changes requested (e.g., Python path |
213 | | and Django settings), then run this command. |
214 | | |
| 214 | and Django settings), then run this command. If the |
| 215 | command raises a ``CommandError``, intercept it and print it sensibly |
| 216 | to stderr. |
215 | 217 | """ |
216 | 218 | parser = self.create_parser(argv[0], argv[1]) |
217 | 219 | options, args = parser.parse_args(argv[2:]) |
218 | 220 | handle_default_options(options) |
219 | | self.execute(*args, **options.__dict__) |
| 221 | try: |
| 222 | self.execute(*args, **options.__dict__) |
| 223 | except Exception as e: |
| 224 | if options.traceback: |
| 225 | self.stderr.write(traceback.format_exc()) |
| 226 | self.stderr.write('%s: %s' % (e.__class__.__name__, e)) |
| 227 | sys.exit(1) |
220 | 228 | |
221 | 229 | def execute(self, *args, **options): |
222 | 230 | """ |
223 | 231 | Try to execute this command, performing model validation if |
224 | 232 | needed (as controlled by the attribute |
225 | | ``self.requires_model_validation``, except if force-skipped). If the |
226 | | command raises a ``CommandError``, intercept it and print it sensibly |
227 | | to stderr. |
| 233 | ``self.requires_model_validation``, except if force-skipped). |
228 | 234 | """ |
229 | | show_traceback = options.get('traceback', False) |
230 | 235 | |
231 | 236 | # Switch to English, because django-admin.py creates database content |
232 | 237 | # like permissions, and those shouldn't contain any translations. |
… |
… |
class BaseCommand(object):
|
237 | 242 | self.stderr = OutputWrapper(options.get('stderr', sys.stderr), self.style.ERROR) |
238 | 243 | |
239 | 244 | if self.can_import_settings: |
240 | | try: |
241 | | from django.utils import translation |
242 | | saved_lang = translation.get_language() |
243 | | translation.activate('en-us') |
244 | | except ImportError as e: |
245 | | # If settings should be available, but aren't, |
246 | | # raise the error and quit. |
247 | | if show_traceback: |
248 | | traceback.print_exc() |
249 | | else: |
250 | | self.stderr.write('Error: %s' % e) |
251 | | sys.exit(1) |
| 245 | from django.utils import translation |
| 246 | saved_lang = translation.get_language() |
| 247 | translation.activate('en-us') |
252 | 248 | |
253 | 249 | try: |
254 | 250 | if self.requires_model_validation and not options.get('skip_validation'): |
… |
… |
class BaseCommand(object):
|
265 | 261 | self.stdout.write(output) |
266 | 262 | if self.output_transaction: |
267 | 263 | self.stdout.write('\n' + self.style.SQL_KEYWORD("COMMIT;")) |
268 | | except CommandError as e: |
269 | | if show_traceback: |
270 | | traceback.print_exc() |
271 | | else: |
272 | | self.stderr.write('Error: %s' % e) |
273 | | sys.exit(1) |
274 | 264 | finally: |
275 | 265 | if saved_lang is not None: |
276 | 266 | translation.activate(saved_lang) |
diff --git a/docs/howto/custom-management-commands.txt b/docs/howto/custom-management-commands.txt
index 2b34d35..4a27bdf 100644
a
|
b
|
Exception class indicating a problem while executing a management
|
317 | 317 | command. |
318 | 318 | |
319 | 319 | If this exception is raised during the execution of a management |
320 | | command, it will be caught and turned into a nicely-printed error |
321 | | message to the appropriate output stream (i.e., stderr); as a |
322 | | result, raising this exception (with a sensible description of the |
| 320 | command from a command line console, it will be caught and turned into a |
| 321 | nicely-printed error message to the appropriate output stream (i.e., stderr); |
| 322 | as a result, raising this exception (with a sensible description of the |
323 | 323 | error) is the preferred way to indicate that something has gone |
324 | 324 | wrong in the execution of a command. |
| 325 | |
| 326 | If a management command is called from code through |
| 327 | :ref:`call_command <call-command>`, it's up to you to catch the exception |
| 328 | when needed. |
diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt
index 0fec53a..4b47255 100644
a
|
b
|
Django 1.5 also includes several smaller improvements worth noting:
|
75 | 75 | |
76 | 76 | * The generic views support OPTIONS requests. |
77 | 77 | |
| 78 | * Management commands do not raise SystemExit any more when called by code from |
| 79 | :ref:`call_command <call-command>`. Any exception raised by the command |
| 80 | (mostly CommandError) is propagated. |
| 81 | |
78 | 82 | * The dumpdata management command outputs one row at a time, preventing |
79 | 83 | out-of-memory errors when dumping large datasets. |
80 | 84 | |
diff --git a/tests/modeltests/fixtures/tests.py b/tests/modeltests/fixtures/tests.py
index 48a5fe7..478bbe9 100644
a
|
b
|
class FixtureLoadingTests(TestCase):
|
185 | 185 | exclude_list=['fixtures.Article', 'fixtures.Book', 'sites']) |
186 | 186 | |
187 | 187 | # Excluding a bogus app should throw an error |
188 | | self.assertRaises(SystemExit, |
| 188 | self.assertRaises(management.CommandError, |
189 | 189 | self._dumpdata_assert, |
190 | 190 | ['fixtures', 'sites'], |
191 | 191 | '', |
192 | 192 | exclude_list=['foo_app']) |
193 | 193 | |
194 | 194 | # Excluding a bogus model should throw an error |
195 | | self.assertRaises(SystemExit, |
| 195 | self.assertRaises(management.CommandError, |
196 | 196 | self._dumpdata_assert, |
197 | 197 | ['fixtures', 'sites'], |
198 | 198 | '', |
diff --git a/tests/modeltests/user_commands/management/commands/dance.py b/tests/modeltests/user_commands/management/commands/dance.py
index 4ad5579..911530d 100644
a
|
b
|
|
1 | 1 | from optparse import make_option |
2 | 2 | |
3 | | from django.core.management.base import BaseCommand |
| 3 | from django.core.management.base import BaseCommand, CommandError |
4 | 4 | |
5 | 5 | |
6 | 6 | class Command(BaseCommand): |
… |
… |
class Command(BaseCommand):
|
8 | 8 | args = '' |
9 | 9 | requires_model_validation = True |
10 | 10 | |
11 | | option_list =[ |
| 11 | option_list = BaseCommand.option_list + ( |
12 | 12 | make_option("-s", "--style", default="Rock'n'Roll"), |
13 | 13 | make_option("-x", "--example") |
14 | | ] |
| 14 | ) |
15 | 15 | |
16 | 16 | def handle(self, *args, **options): |
17 | 17 | example = options["example"] |
| 18 | if example == "raise": |
| 19 | raise CommandError() |
18 | 20 | self.stdout.write("I don't feel like dancing %s." % options["style"]) |
diff --git a/tests/modeltests/user_commands/tests.py b/tests/modeltests/user_commands/tests.py
index c1e2bf9..509f13f 100644
a
|
b
|
|
| 1 | import sys |
1 | 2 | from StringIO import StringIO |
2 | 3 | |
3 | 4 | from django.core import management |
… |
… |
class CommandTests(TestCase):
|
26 | 27 | self.assertEqual(translation.get_language(), 'fr') |
27 | 28 | |
28 | 29 | def test_explode(self): |
| 30 | """ Test that an unknown command raises CommandError """ |
29 | 31 | self.assertRaises(CommandError, management.call_command, ('explode',)) |
| 32 | |
| 33 | def test_system_exit(self): |
| 34 | """ Exception raised in a command should raise CommandError with |
| 35 | call_command, but SystemExit when run from command line |
| 36 | """ |
| 37 | with self.assertRaises(CommandError): |
| 38 | management.call_command('dance', example="raise") |
| 39 | old_stderr = sys.stderr |
| 40 | sys.stderr = err = StringIO() |
| 41 | try: |
| 42 | with self.assertRaises(SystemExit): |
| 43 | management.ManagementUtility(['manage.py', 'dance', '--example=raise']).execute() |
| 44 | finally: |
| 45 | sys.stderr = old_stderr |
| 46 | self.assertIn("CommandError", err.getvalue()) |