commit a6ed2cf0f2f5ba30f6753e6ac38337d570050aad
Author: Claude Paroz <claude@2xlibre.net>
Date:   Sat May 5 21:01:12 2012 +0200

    Set output of commands through logging methods

diff --git a/django/contrib/auth/management/commands/changepassword.py b/django/contrib/auth/management/commands/changepassword.py
index d125dfe..d46cb4f 100644
--- a/django/contrib/auth/management/commands/changepassword.py
+++ b/django/contrib/auth/management/commands/changepassword.py
@@ -35,7 +35,7 @@ class Command(BaseCommand):
         except User.DoesNotExist:
             raise CommandError("user '%s' does not exist" % username)
 
-        self.stdout.write("Changing password for user '%s'\n" % u.username)
+        self.info("Changing password for user '%s'" % u.username)
 
         MAX_TRIES = 3
         count = 0
@@ -44,7 +44,7 @@ class Command(BaseCommand):
             p1 = self._get_pass()
             p2 = self._get_pass("Password (again): ")
             if p1 != p2:
-                self.stdout.write("Passwords do not match. Please try again.\n")
+                self.info("Passwords do not match. Please try again.")
                 count = count + 1
 
         if count == MAX_TRIES:
diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py
index ad65977..99e2e8e 100644
--- a/django/contrib/auth/management/commands/createsuperuser.py
+++ b/django/contrib/auth/management/commands/createsuperuser.py
@@ -80,7 +80,7 @@ class Command(BaseCommand):
                     if default_username and username == '':
                         username = default_username
                     if not RE_VALID_USERNAME.match(username):
-                        sys.stderr.write("Error: That username is invalid. Use only letters, digits and underscores.\n")
+                        self.error("Error: That username is invalid. Use only letters, digits and underscores.")
                         username = None
                         continue
                     try:
@@ -88,7 +88,7 @@ class Command(BaseCommand):
                     except User.DoesNotExist:
                         break
                     else:
-                        sys.stderr.write("Error: That username is already taken.\n")
+                        self.error("Error: That username is already taken.")
                         username = None
 
                 # Get an email
@@ -98,7 +98,7 @@ class Command(BaseCommand):
                     try:
                         is_valid_email(email)
                     except exceptions.ValidationError:
-                        sys.stderr.write("Error: That e-mail address is invalid.\n")
+                        self.error("Error: That e-mail address is invalid.")
                         email = None
                     else:
                         break
@@ -109,19 +109,18 @@ class Command(BaseCommand):
                         password = getpass.getpass()
                         password2 = getpass.getpass('Password (again): ')
                         if password != password2:
-                            sys.stderr.write("Error: Your passwords didn't match.\n")
+                            self.error("Error: Your passwords didn't match.")
                             password = None
                             continue
                     if password.strip() == '':
-                        sys.stderr.write("Error: Blank passwords aren't allowed.\n")
+                        self.error("Error: Blank passwords aren't allowed.")
                         password = None
                         continue
                     break
             except KeyboardInterrupt:
-                sys.stderr.write("\nOperation cancelled.\n")
+                self.error("\nOperation cancelled.")
                 sys.exit(1)
 
         User.objects.db_manager(database).create_superuser(username, email, password)
         if verbosity >= 1:
-          self.stdout.write("Superuser created successfully.\n")
-
+          self.info("Superuser created successfully.")
diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py
index cab96c8..499a57a 100644
--- a/django/contrib/staticfiles/management/commands/collectstatic.py
+++ b/django/contrib/staticfiles/management/commands/collectstatic.py
@@ -165,7 +165,7 @@ Type 'yes' to continue, or 'no' to cancel: """
 
         if self.verbosity >= 1:
             template = ("\n%(modified_count)s %(identifier)s %(action)s"
-                        "%(destination)s%(unmodified)s%(post_processed)s.\n")
+                        "%(destination)s%(unmodified)s%(post_processed)s.")
             summary = template % {
                 'modified_count': modified_count,
                 'identifier': 'static file' + (modified_count != 1 and 's' or ''),
@@ -178,17 +178,14 @@ Type 'yes' to continue, or 'no' to cancel: """
                                    ', %s post-processed'
                                    % post_processed_count or ''),
             }
-            self.stdout.write(smart_str(summary))
+            self.info(smart_str(summary))
 
     def log(self, msg, level=2):
         """
         Small log helper
         """
-        msg = smart_str(msg)
-        if not msg.endswith("\n"):
-            msg += "\n"
         if self.verbosity >= level:
-            self.stdout.write(msg)
+            self.info(smart_str(msg))
 
     def clear_dir(self, path):
         """
diff --git a/django/contrib/staticfiles/management/commands/findstatic.py b/django/contrib/staticfiles/management/commands/findstatic.py
index bcf0c2f..77d23d7 100644
--- a/django/contrib/staticfiles/management/commands/findstatic.py
+++ b/django/contrib/staticfiles/management/commands/findstatic.py
@@ -23,9 +23,8 @@ class Command(LabelCommand):
                 result = [result]
             output = u'\n  '.join(
                 (smart_unicode(os.path.realpath(path)) for path in result))
-            self.stdout.write(
-                smart_str(u"Found '%s' here:\n  %s\n" % (path, output)))
+            self.info(
+                smart_str(u"Found '%s' here:\n  %s" % (path, output)))
         else:
             if verbosity >= 1:
-                self.stderr.write(
-                    smart_str("No matching file found for '%s'.\n" % path))
+                self.error("No matching file found for '%s'." % path)
diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py
index 0464eb2..d831858 100644
--- a/django/core/management/__init__.py
+++ b/django/core/management/__init__.py
@@ -8,6 +8,7 @@ import warnings
 from django.core.management.base import BaseCommand, CommandError, handle_default_options
 from django.core.management.color import color_style
 from django.utils.importlib import import_module
+from django.utils.log import getLogger
 
 # For backwards compatibility: get_version() used to be in this module.
 from django import get_version
@@ -15,6 +16,7 @@ from django import get_version
 # A cache of loaded commands, so that call_command
 # doesn't have to reload every time it's called.
 _commands = None
+logger = getLogger('django.commands')
 
 def find_commands(management_dir):
     """
@@ -139,7 +141,7 @@ def call_command(name, *args, **options):
     # when the script runs from the command line, but since call_command can
     # be called programatically, we need to simulate the loading and handling
     # of defaults (see #10080 for details).
-    defaults = {}
+    defaults = {'raise_on_error': True}
     for opt in klass.option_list:
         if opt.default is NO_DEFAULT:
             defaults[opt.dest] = None
@@ -149,6 +151,7 @@ def call_command(name, *args, **options):
 
     return klass.execute(*args, **defaults)
 
+
 class LaxOptionParser(OptionParser):
     """
     An option parser that doesn't raise any errors on unknown options.
@@ -251,7 +254,7 @@ class ManagementUtility(object):
         try:
             app_name = get_commands()[subcommand]
         except KeyError:
-            sys.stderr.write("Unknown command: %r\nType '%s help' for usage.\n" % \
+            logger.error("Unknown command: %r\nType '%s help' for usage." % \
                 (subcommand, self.prog_name))
             sys.exit(1)
         if isinstance(app_name, BaseCommand):
@@ -362,13 +365,13 @@ class ManagementUtility(object):
         if subcommand == 'help':
             if len(args) <= 2:
                 parser.print_lax_help()
-                sys.stdout.write(self.main_help_text() + '\n')
+                logger.info(self.main_help_text())
             elif args[2] == '--commands':
-                sys.stdout.write(self.main_help_text(commands_only=True) + '\n')
+                logger.info(self.main_help_text(commands_only=True))
             else:
                 self.fetch_command(args[2]).print_help(self.prog_name, args[2])
         elif subcommand == 'version':
-            sys.stdout.write(parser.get_version() + '\n')
+            logger.info(parser.get_version())
         # Special-cases: We want 'django-admin.py --version' and
         # 'django-admin.py --help' to work, for backwards compatibility.
         elif self.argv[1:] == ['--version']:
@@ -376,7 +379,7 @@ class ManagementUtility(object):
             pass
         elif self.argv[1:] in (['--help'], ['-h']):
             parser.print_lax_help()
-            sys.stdout.write(self.main_help_text() + '\n')
+            logger.info(self.main_help_text())
         else:
             self.fetch_command(subcommand).run_from_argv(self.argv)
 
diff --git a/django/core/management/base.py b/django/core/management/base.py
index a936877..d91a2d3 100644
--- a/django/core/management/base.py
+++ b/django/core/management/base.py
@@ -8,11 +8,13 @@ import sys
 
 from optparse import make_option, OptionParser
 import traceback
+import warnings
 
 import django
 from django.core.exceptions import ImproperlyConfigured
 from django.core.management.color import color_style
 from django.utils.encoding import smart_str
+from django.utils.log import getLogger, setup_commands_logger
 
 
 class CommandError(Exception):
@@ -44,6 +46,24 @@ def handle_default_options(options):
         sys.path.insert(0, options.pythonpath)
 
 
+class OutputRedirector(object):
+    """
+    Utility redirector class used until self.stderr and self.sdtout are
+    removed from BaseCommand class (1.7)
+    """
+    def __init__(self, proxy_func):
+        self.proxy_func = proxy_func
+
+    def write(self, msg):
+        warnings.warn(
+            "The 'self.stdout' and 'self.stderr' objects are deprecated, "
+            "update your management command to use the new info() and error() "
+            "methods; please see the Django 1.5 release notes "
+            "(https://docs.djangoproject.com/en/dev/releases/1.5/).",
+            PendingDeprecationWarning)
+        self.proxy_func(msg)
+
+
 class BaseCommand(object):
     """
     The base class from which all management commands ultimately
@@ -141,6 +161,7 @@ class BaseCommand(object):
 
     def __init__(self):
         self.style = color_style()
+        self._logger = getLogger('django.commands')
 
     def get_version(self):
         """
@@ -220,12 +241,20 @@ class BaseCommand(object):
                 if show_traceback:
                     traceback.print_exc()
                 else:
-                    sys.stderr.write(smart_str(self.style.ERROR('Error: %s\n' % e)))
+                    self.error('Error: %s' % e)
                 sys.exit(1)
 
+        if 'stdout' in options or 'stderr' in options:
+            # Set up a temporary logger
+            self._logger = setup_commands_logger(
+                'django.commands_temp',
+                options.get('stdout', sys.stdout),
+                options.get('stderr', sys.stderr)
+            )
+        # Temporarily redirect, until deprecation path is over (1.6)
+        self.stdout = OutputRedirector(self.info)
+        self.stderr = OutputRedirector(self.error)
         try:
-            self.stdout = options.get('stdout', sys.stdout)
-            self.stderr = options.get('stderr', sys.stderr)
             if self.requires_model_validation and not options.get('skip_validation'):
                 self.validate()
             output = self.handle(*args, **options)
@@ -236,17 +265,25 @@ class BaseCommand(object):
                     from django.db import connections, DEFAULT_DB_ALIAS
                     connection = connections[options.get('database', DEFAULT_DB_ALIAS)]
                     if connection.ops.start_transaction_sql():
-                        self.stdout.write(self.style.SQL_KEYWORD(connection.ops.start_transaction_sql()) + '\n')
-                self.stdout.write(output)
-                if self.output_transaction:
-                    self.stdout.write('\n' + self.style.SQL_KEYWORD("COMMIT;") + '\n')
+                        output = "%s\n%s" % (
+                            self.style.SQL_KEYWORD(connection.ops.start_transaction_sql()),
+                            output
+                        )
+                    output = "%s\n%s" % (output, self.style.SQL_KEYWORD("COMMIT;"))
+                self.info(output)
         except CommandError as e:
-            if show_traceback:
-                traceback.print_exc()
+            if options.get('raise_on_error', False):
+                raise
             else:
-                self.stderr.write(smart_str(self.style.ERROR('Error: %s\n' % e)))
-            sys.exit(1)
+                if show_traceback:
+                    traceback.print_exc()
+                else:
+                    self.error('Error: %s' % e)
+                sys.exit(1)
         finally:
+            if self._logger.name == 'django.commands_temp':
+                for h in self._logger.handlers:
+                    h.close()
             if saved_lang is not None:
                 translation.activate(saved_lang)
 
@@ -269,7 +306,15 @@ class BaseCommand(object):
             error_text = s.read()
             raise CommandError("One or more models did not validate:\n%s" % error_text)
         if display_num_errors:
-            self.stdout.write("%s error%s found\n" % (num_errors, num_errors != 1 and 's' or ''))
+            self.info("%s error%s found" % (num_errors, num_errors != 1 and 's' or ''))
+
+    def info(self, msg):
+        """ Output a message on the logger (stdout by default) """
+        self._logger.info(msg)
+
+    def error(self, msg):
+        """ Output an error on the logger (stderr by default) """
+        self._logger.error(smart_str(self.style.ERROR(msg)))
 
     def handle(self, *args, **options):
         """
diff --git a/django/core/management/commands/compilemessages.py b/django/core/management/commands/compilemessages.py
index 354787e..b1e5f48 100644
--- a/django/core/management/commands/compilemessages.py
+++ b/django/core/management/commands/compilemessages.py
@@ -11,7 +11,7 @@ def has_bom(fn):
             sample.startswith(codecs.BOM_UTF16_LE) or \
             sample.startswith(codecs.BOM_UTF16_BE)
 
-def compile_messages(stderr, locale=None):
+def compile_messages(logger, locale=None):
     basedirs = [os.path.join('conf', 'locale'), 'locale']
     if os.environ.get('DJANGO_SETTINGS_MODULE'):
         from django.conf import settings
@@ -29,7 +29,7 @@ def compile_messages(stderr, locale=None):
         for dirpath, dirnames, filenames in os.walk(basedir):
             for f in filenames:
                 if f.endswith('.po'):
-                    stderr.write('processing file %s in %s\n' % (f, dirpath))
+                    logger.info('processing file %s in %s' % (f, dirpath))
                     fn = os.path.join(dirpath, f)
                     if has_bom(fn):
                         raise CommandError("The %s file has a BOM (Byte Order Mark). Django only supports .po files encoded in UTF-8 and without any BOM." % fn)
@@ -60,4 +60,4 @@ class Command(BaseCommand):
 
     def handle(self, **options):
         locale = options.get('locale')
-        compile_messages(self.stderr, locale=locale)
+        compile_messages(self._logger, locale=locale)
diff --git a/django/core/management/commands/createcachetable.py b/django/core/management/commands/createcachetable.py
index 82a126b..403722a 100644
--- a/django/core/management/commands/createcachetable.py
+++ b/django/core/management/commands/createcachetable.py
@@ -1,7 +1,7 @@
 from optparse import make_option
 
 from django.core.cache.backends.db import BaseDatabaseCache
-from django.core.management.base import LabelCommand
+from django.core.management.base import LabelCommand, CommandError
 from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS
 from django.db.utils import DatabaseError
 
@@ -55,10 +55,9 @@ class Command(LabelCommand):
         try:
             curs.execute("\n".join(full_statement))
         except DatabaseError as e:
-            self.stderr.write(
-                self.style.ERROR("Cache table '%s' could not be created.\nThe error was: %s.\n" %
-                    (tablename, e)))
             transaction.rollback_unless_managed(using=db)
+            raise CommandError("Cache table '%s' could not be created.\nThe error was: %s." % (
+                tablename, e))
         else:
             for statement in index_output:
                 curs.execute(statement)
diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py
index ce3c6e8..a6c1a8b 100644
--- a/django/core/management/commands/flush.py
+++ b/django/core/management/commands/flush.py
@@ -28,8 +28,6 @@ class Command(NoArgsCommand):
         verbosity = int(options.get('verbosity'))
         interactive = options.get('interactive')
 
-        self.style = no_style()
-
         # Import the 'management' module within each installed app, to register
         # dispatcher events.
         for app_name in settings.INSTALLED_APPS:
@@ -38,7 +36,7 @@ class Command(NoArgsCommand):
             except ImportError:
                 pass
 
-        sql_list = sql_flush(self.style, connection, only_django=True)
+        sql_list = sql_flush(no_style(), connection, only_django=True)
 
         if interactive:
             confirm = raw_input("""You have requested a flush of the database.
@@ -82,4 +80,4 @@ The full error: %s""" % (connection.settings_dict['NAME'], e))
             call_command('loaddata', 'initial_data', **kwargs)
 
         else:
-            self.stdout.write("Flush cancelled.\n")
+            return "Flush cancelled."
diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py
index 10fd9bd..1220186 100644
--- a/django/core/management/commands/inspectdb.py
+++ b/django/core/management/commands/inspectdb.py
@@ -20,7 +20,7 @@ class Command(NoArgsCommand):
     def handle_noargs(self, **options):
         try:
             for line in self.handle_inspection(options):
-                self.stdout.write("%s\n" % line)
+                self.info(line)
         except NotImplementedError:
             raise CommandError("Database inspection isn't supported for the currently selected database backend.")
 
diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py
index 848b637..05bc211 100644
--- a/django/core/management/commands/loaddata.py
+++ b/django/core/management/commands/loaddata.py
@@ -7,7 +7,7 @@ import traceback
 
 from django.conf import settings
 from django.core import serializers
-from django.core.management.base import BaseCommand
+from django.core.management.base import BaseCommand, CommandError
 from django.core.management.color import no_style
 from django.db import (connections, router, transaction, DEFAULT_DB_ALIAS,
       IntegrityError, DatabaseError)
@@ -20,6 +20,7 @@ try:
 except ImportError:
     has_bz2 = False
 
+
 class Command(BaseCommand):
     help = 'Installs the named fixture(s) in the database.'
     args = "fixture [fixture ...]"
@@ -34,13 +35,10 @@ class Command(BaseCommand):
         using = options.get('database')
 
         connection = connections[using]
-        self.style = no_style()
 
         if not len(fixture_labels):
-            self.stderr.write(
-                self.style.ERROR("No database fixture specified. Please provide the path of at least one fixture in the command line.\n")
-            )
-            return
+            raise CommandError("No database fixture specified. "
+                "Please provide the path of at least one fixture in the command line.")
 
         verbosity = int(options.get('verbosity'))
         show_traceback = options.get('traceback')
@@ -124,15 +122,10 @@ class Command(BaseCommand):
 
                     if formats:
                         if verbosity >= 2:
-                            self.stdout.write("Loading '%s' fixtures...\n" % fixture_name)
+                            self.info("Loading '%s' fixtures..." % fixture_name)
                     else:
-                        self.stderr.write(
-                            self.style.ERROR("Problem installing fixture '%s': %s is not a known serialization format.\n" %
-                                (fixture_name, format)))
-                        if commit:
-                            transaction.rollback(using=using)
-                            transaction.leave_transaction_management(using=using)
-                        return
+                        raise CommandError("Problem installing fixture '%s': %s is not a known serialization format." %
+                            (fixture_name, format))
 
                     if os.path.isabs(fixture_name):
                         fixture_dirs = [fixture_name]
@@ -141,7 +134,7 @@ class Command(BaseCommand):
 
                     for fixture_dir in fixture_dirs:
                         if verbosity >= 2:
-                            self.stdout.write("Checking %s for fixtures...\n" % humanize(fixture_dir))
+                            self.info("Checking %s for fixtures..." % humanize(fixture_dir))
 
                         label_found = False
                         for combo in product([using, None], formats, compression_formats):
@@ -154,32 +147,28 @@ class Command(BaseCommand):
                             )
 
                             if verbosity >= 3:
-                                self.stdout.write("Trying %s for %s fixture '%s'...\n" % \
-                                    (humanize(fixture_dir), file_name, fixture_name))
+                                self.info("Trying %s for %s fixture '%s'..." % (
+                                    humanize(fixture_dir), file_name, fixture_name))
                             full_path = os.path.join(fixture_dir, file_name)
                             open_method = compression_types[compression_format]
                             try:
                                 fixture = open_method(full_path, 'r')
                             except IOError:
                                 if verbosity >= 2:
-                                    self.stdout.write("No %s fixture '%s' in %s.\n" % \
-                                        (format, fixture_name, humanize(fixture_dir)))
+                                    self.info("No %s fixture '%s' in %s." % (
+                                        format, fixture_name, humanize(fixture_dir)))
                             else:
                                 try:
                                     if label_found:
-                                        self.stderr.write(self.style.ERROR("Multiple fixtures named '%s' in %s. Aborting.\n" %
-                                            (fixture_name, humanize(fixture_dir))))
-                                        if commit:
-                                            transaction.rollback(using=using)
-                                            transaction.leave_transaction_management(using=using)
-                                        return
+                                        raise CommandError("Multiple fixtures named '%s' in %s. Aborting." %
+                                            (fixture_name, humanize(fixture_dir)))
 
                                     fixture_count += 1
                                     objects_in_fixture = 0
                                     loaded_objects_in_fixture = 0
                                     if verbosity >= 2:
-                                        self.stdout.write("Installing %s fixture '%s' from %s.\n" % \
-                                            (format, fixture_name, humanize(fixture_dir)))
+                                        self.info("Installing %s fixture '%s' from %s." % (
+                                            format, fixture_name, humanize(fixture_dir)))
 
                                     objects = serializers.deserialize(format, fixture, using=using)
 
@@ -208,13 +197,8 @@ class Command(BaseCommand):
                                 # If the fixture we loaded contains 0 objects, assume that an
                                 # error was encountered during fixture loading.
                                 if objects_in_fixture == 0:
-                                    self.stderr.write(
-                                        self.style.ERROR("No fixture data found for '%s'. (File format may be invalid.)\n" %
-                                            (fixture_name)))
-                                    if commit:
-                                        transaction.rollback(using=using)
-                                        transaction.leave_transaction_management(using=using)
-                                    return
+                                    raise CommandError("No fixture data found for '%s'. (File format may be invalid.)" %
+                                        (fixture_name))
 
             # Since we disabled constraint checks, we must manually check for
             # any invalid keys that might have been added
@@ -223,6 +207,11 @@ class Command(BaseCommand):
 
         except (SystemExit, KeyboardInterrupt):
             raise
+        except CommandError:
+            if commit:
+                transaction.rollback(using=using)
+                transaction.leave_transaction_management(using=using)
+            raise
         except Exception:
             if commit:
                 transaction.rollback(using=using)
@@ -230,20 +219,19 @@ class Command(BaseCommand):
             if show_traceback:
                 traceback.print_exc()
             else:
-                self.stderr.write(
-                    self.style.ERROR("Problem installing fixture '%s': %s\n" %
-                         (full_path, ''.join(traceback.format_exception(sys.exc_type,
-                             sys.exc_value, sys.exc_traceback)))))
+                self.error("Problem installing fixture '%s': %s" % (
+                           full_path, ''.join(traceback.format_exception(sys.exc_type,
+                           sys.exc_value, sys.exc_traceback))))
             return
 
 
         # If we found even one object in a fixture, we need to reset the
         # database sequences.
         if loaded_object_count > 0:
-            sequence_sql = connection.ops.sequence_reset_sql(self.style, models)
+            sequence_sql = connection.ops.sequence_reset_sql(no_style(), models)
             if sequence_sql:
                 if verbosity >= 2:
-                    self.stdout.write("Resetting sequences\n")
+                    self.info("Resetting sequences")
                 for line in sequence_sql:
                     cursor.execute(line)
 
@@ -253,10 +241,10 @@ class Command(BaseCommand):
 
         if verbosity >= 1:
             if fixture_object_count == loaded_object_count:
-                self.stdout.write("Installed %d object(s) from %d fixture(s)\n" % (
+                self.info("Installed %d object(s) from %d fixture(s)" % (
                     loaded_object_count, fixture_count))
             else:
-                self.stdout.write("Installed %d object(s) (of %d) from %d fixture(s)\n" % (
+                self.info("Installed %d object(s) (of %d) from %d fixture(s)" % (
                     loaded_object_count, fixture_object_count, fixture_count))
 
         # Close the DB connection. This is required as a workaround for an
diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py
index 046ffb4..bba43d3 100644
--- a/django/core/management/commands/makemessages.py
+++ b/django/core/management/commands/makemessages.py
@@ -46,7 +46,7 @@ def _popen(cmd):
     return p.communicate()
 
 def walk(root, topdown=True, onerror=None, followlinks=False,
-         ignore_patterns=None, verbosity=0, stdout=sys.stdout):
+         ignore_patterns=None, verbosity=0, logger=None):
     """
     A version of os.walk that can follow symlinks for Python < 2.6
     """
@@ -63,7 +63,7 @@ def walk(root, topdown=True, onerror=None, followlinks=False,
         for dirname in remove_dirs:
             dirnames.remove(dirname)
             if verbosity > 1:
-                stdout.write('ignoring directory %s\n' % dirname)
+                logger.info('ignoring directory %s' % dirname)
         yield (dirpath, dirnames, filenames)
         if followlinks:
             for d in dirnames:
@@ -81,24 +81,24 @@ def is_ignored(path, ignore_patterns):
             return True
     return False
 
-def find_files(root, ignore_patterns, verbosity, stdout=sys.stdout, symlinks=False):
+def find_files(root, ignore_patterns, verbosity, logger, symlinks=False):
     """
     Helper function to get all files in the given root.
     """
     all_files = []
     for (dirpath, dirnames, filenames) in walk(root, followlinks=symlinks,
-            ignore_patterns=ignore_patterns, verbosity=verbosity, stdout=stdout):
+            ignore_patterns=ignore_patterns, verbosity=verbosity, logger=logger):
         for filename in filenames:
             norm_filepath = os.path.normpath(os.path.join(dirpath, filename))
             if is_ignored(norm_filepath, ignore_patterns):
                 if verbosity > 1:
-                    stdout.write('ignoring file %s in %s\n' % (filename, dirpath))
+                    logger.info('ignoring file %s in %s' % (filename, dirpath))
             else:
                 all_files.extend([(dirpath, filename)])
     all_files.sort()
     return all_files
 
-def copy_plural_forms(msgs, locale, domain, verbosity, stdout=sys.stdout):
+def copy_plural_forms(msgs, locale, domain, verbosity, logger):
     """
     Copies plural forms header contents from a Django catalog of locale to
     the msgs string, inserting it at the right place. msgs should be the
@@ -116,7 +116,7 @@ def copy_plural_forms(msgs, locale, domain, verbosity, stdout=sys.stdout):
                 m = plural_forms_re.search(fp.read())
             if m:
                 if verbosity > 1:
-                    stdout.write("copying plural forms: %s\n" % m.group('value'))
+                    logger.info("copying plural forms: %s" % m.group('value'))
                 lines = []
                 seen = False
                 for line in msgs.split('\n'):
@@ -146,7 +146,7 @@ def write_pot_file(potfile, msgs, file, work_file, is_templatized):
         fp.write(msgs)
 
 def process_file(file, dirpath, potfile, domain, verbosity,
-                 extensions, wrap, location, stdout=sys.stdout):
+                 extensions, wrap, location, logger):
     """
     Extract translatable literals from :param file: for :param domain:
     creating or updating the :param potfile: POT file.
@@ -157,7 +157,7 @@ def process_file(file, dirpath, potfile, domain, verbosity,
     from django.utils.translation import templatize
 
     if verbosity > 1:
-        stdout.write('processing file %s in %s\n' % (file, dirpath))
+        logger.info('processing file %s in %s' % (file, dirpath))
     _, file_ext = os.path.splitext(file)
     if domain == 'djangojs' and file_ext in extensions:
         is_templatized = True
@@ -212,7 +212,7 @@ def process_file(file, dirpath, potfile, domain, verbosity,
     if is_templatized:
         os.unlink(work_file)
 
-def write_po_file(pofile, potfile, domain, locale, verbosity, stdout,
+def write_po_file(pofile, potfile, domain, locale, verbosity, logger,
                   copy_pforms, wrap, location, no_obsolete):
     """
     Creates of updates the :param pofile: PO file for :param domain: and :param
@@ -235,7 +235,7 @@ def write_po_file(pofile, potfile, domain, locale, verbosity, stdout,
             raise CommandError(
                 "errors happened while running msgmerge\n%s" % errors)
     elif copy_pforms:
-        msgs = copy_plural_forms(msgs, locale, domain, verbosity, stdout)
+        msgs = copy_plural_forms(msgs, locale, domain, verbosity, logger)
     msgs = msgs.replace(
         "#. #-#-#-#-#  %s.pot (PACKAGE VERSION)  #-#-#-#-#\n" % domain, "")
     with open(pofile, 'wb') as fp:
@@ -250,7 +250,7 @@ def write_po_file(pofile, potfile, domain, locale, verbosity, stdout,
 
 def make_messages(locale=None, domain='django', verbosity=1, all=False,
         extensions=None, symlinks=False, ignore_patterns=None, no_wrap=False,
-        no_location=False, no_obsolete=False, stdout=sys.stdout):
+        no_location=False, no_obsolete=False, logger=None):
     """
     Uses the ``locale/`` directory from the Django Git tree or an
     application/project to process all files with translatable literals for
@@ -312,7 +312,7 @@ def make_messages(locale=None, domain='django', verbosity=1, all=False,
 
     for locale in locales:
         if verbosity > 0:
-            stdout.write("processing language %s\n" % locale)
+            logger.info("processing language %s" % locale)
         basedir = os.path.join(localedir, locale, 'LC_MESSAGES')
         if not os.path.isdir(basedir):
             os.makedirs(basedir)
@@ -324,12 +324,12 @@ def make_messages(locale=None, domain='django', verbosity=1, all=False,
             os.unlink(potfile)
 
         for dirpath, file in find_files(".", ignore_patterns, verbosity,
-                stdout, symlinks=symlinks):
+                logger, symlinks=symlinks):
             process_file(file, dirpath, potfile, domain, verbosity, extensions,
-                    wrap, location, stdout)
+                    wrap, location, logger)
 
         if os.path.exists(potfile):
-            write_po_file(pofile, potfile, domain, locale, verbosity, stdout,
+            write_po_file(pofile, potfile, domain, locale, verbosity, logger,
                     not invoked_for_django, wrap, location, no_obsolete)
 
 
@@ -387,8 +387,8 @@ class Command(NoArgsCommand):
         extensions = handle_extensions(exts)
 
         if verbosity > 1:
-            self.stdout.write('examining files with the extensions: %s\n'
-                             % get_text_list(list(extensions), 'and'))
+            self.info('examining files with the extensions: %s'
+                      % get_text_list(list(extensions), 'and'))
 
         make_messages(locale, domain, verbosity, process_all, extensions,
-            symlinks, ignore_patterns, no_wrap, no_location, no_obsolete, self.stdout)
+            symlinks, ignore_patterns, no_wrap, no_location, no_obsolete, self._logger)
diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py
index e618219..f4b2bf1 100644
--- a/django/core/management/commands/runserver.py
+++ b/django/core/management/commands/runserver.py
@@ -87,19 +87,19 @@ class Command(BaseCommand):
         shutdown_message = options.get('shutdown_message', '')
         quit_command = (sys.platform == 'win32') and 'CTRL-BREAK' or 'CONTROL-C'
 
-        self.stdout.write("Validating models...\n\n")
+        self.info("Validating models...\n")
         self.validate(display_num_errors=True)
-        self.stdout.write((
+        self.info(
             "Django version %(version)s, using settings %(settings)r\n"
             "Development server is running at http://%(addr)s:%(port)s/\n"
-            "Quit the server with %(quit_command)s.\n"
-        ) % {
-            "version": self.get_version(),
-            "settings": settings.SETTINGS_MODULE,
-            "addr": self._raw_ipv6 and '[%s]' % self.addr or self.addr,
-            "port": self.port,
-            "quit_command": quit_command,
-        })
+            "Quit the server with %(quit_command)s.\n" % {
+                "version": self.get_version(),
+                "settings": settings.SETTINGS_MODULE,
+                "addr": self._raw_ipv6 and '[%s]' % self.addr or self.addr,
+                "port": self.port,
+                "quit_command": quit_command,
+            }
+        )
         # django.core.management.base forces the locale to en-us. We should
         # set it up correctly for the first request (particularly important
         # in the "--noreload" case).
@@ -120,12 +120,12 @@ class Command(BaseCommand):
                 error_text = ERRORS[e.args[0].args[0]]
             except (AttributeError, KeyError):
                 error_text = str(e)
-            sys.stderr.write(self.style.ERROR("Error: %s" % error_text) + '\n')
+            self.error("Error: %s" % error_text)
             # Need to use an OS exit because sys.exit doesn't work in a thread
             os._exit(1)
         except KeyboardInterrupt:
             if shutdown_message:
-                self.stdout.write("%s\n" % shutdown_message)
+                self.info(shutdown_message)
             sys.exit(0)
 
 
diff --git a/django/core/management/commands/syncdb.py b/django/core/management/commands/syncdb.py
index 88caea1..c7d172a 100644
--- a/django/core/management/commands/syncdb.py
+++ b/django/core/management/commands/syncdb.py
@@ -81,12 +81,12 @@ class Command(NoArgsCommand):
 
         # Create the tables for each model
         if verbosity >= 1:
-            self.stdout.write("Creating tables ...\n")
+            self.info("Creating tables ...")
         for app_name, model_list in manifest.items():
             for model in model_list:
                 # Create the model's database table, if it doesn't already exist.
                 if verbosity >= 3:
-                    self.stdout.write("Processing %s.%s model\n" % (app_name, model._meta.object_name))
+                    self.info("Processing %s.%s model" % (app_name, model._meta.object_name))
                 sql, references = connection.creation.sql_create_model(model, self.style, seen_models)
                 seen_models.add(model)
                 created_models.add(model)
@@ -96,7 +96,7 @@ class Command(NoArgsCommand):
                         sql.extend(connection.creation.sql_for_pending_references(refto, self.style, pending_references))
                 sql.extend(connection.creation.sql_for_pending_references(model, self.style, pending_references))
                 if verbosity >= 1 and sql:
-                    self.stdout.write("Creating table %s\n" % model._meta.db_table)
+                    self.info("Creating table %s" % model._meta.db_table)
                 for statement in sql:
                     cursor.execute(statement)
                 tables.append(connection.introspection.table_name_converter(model._meta.db_table))
@@ -114,20 +114,21 @@ class Command(NoArgsCommand):
         # Install custom SQL for the app (but only if this
         # is a model we've just created)
         if verbosity >= 1:
-            self.stdout.write("Installing custom SQL ...\n")
+            self.info("Installing custom SQL ...")
         for app_name, model_list in manifest.items():
             for model in model_list:
                 if model in created_models:
                     custom_sql = custom_sql_for_model(model, self.style, connection)
                     if custom_sql:
                         if verbosity >= 2:
-                            self.stdout.write("Installing custom SQL for %s.%s model\n" % (app_name, model._meta.object_name))
+                            self.info("Installing custom SQL for %s.%s model" % (
+                                app_name, model._meta.object_name))
                         try:
                             for sql in custom_sql:
                                 cursor.execute(sql)
                         except Exception as e:
-                            self.stderr.write("Failed to install custom SQL for %s.%s model: %s\n" % \
-                                                (app_name, model._meta.object_name, e))
+                            self.error("Failed to install custom SQL for %s.%s model: %s" % (
+                                       app_name, model._meta.object_name, e))
                             if show_traceback:
                                 traceback.print_exc()
                             transaction.rollback_unless_managed(using=db)
@@ -135,10 +136,11 @@ class Command(NoArgsCommand):
                             transaction.commit_unless_managed(using=db)
                     else:
                         if verbosity >= 3:
-                            self.stdout.write("No custom SQL for %s.%s model\n" % (app_name, model._meta.object_name))
+                            self.info("No custom SQL for %s.%s model" % (
+                                app_name, model._meta.object_name))
 
         if verbosity >= 1:
-            self.stdout.write("Installing indexes ...\n")
+            self.info("Installing indexes ...")
         # Install SQL indices for all newly created models
         for app_name, model_list in manifest.items():
             for model in model_list:
@@ -146,13 +148,14 @@ class Command(NoArgsCommand):
                     index_sql = connection.creation.sql_indexes_for_model(model, self.style)
                     if index_sql:
                         if verbosity >= 2:
-                            self.stdout.write("Installing index for %s.%s model\n" % (app_name, model._meta.object_name))
+                            self.info("Installing index for %s.%s model" % (
+                                app_name, model._meta.object_name))
                         try:
                             for sql in index_sql:
                                 cursor.execute(sql)
                         except Exception as e:
-                            self.stderr.write("Failed to install index for %s.%s model: %s\n" % \
-                                                (app_name, model._meta.object_name, e))
+                            self.error("Failed to install index for %s.%s model: %s" % (
+                                                app_name, model._meta.object_name, e))
                             transaction.rollback_unless_managed(using=db)
                         else:
                             transaction.commit_unless_managed(using=db)
diff --git a/django/core/management/templates.py b/django/core/management/templates.py
index 735e29a..6955a7b 100644
--- a/django/core/management/templates.py
+++ b/django/core/management/templates.py
@@ -99,12 +99,10 @@ class TemplateCommand(BaseCommand):
         for file in options.get('files'):
             extra_files.extend(map(lambda x: x.strip(), file.split(',')))
         if self.verbosity >= 2:
-            self.stdout.write("Rendering %s template files with "
-                              "extensions: %s\n" %
-                              (app_or_project, ', '.join(extensions)))
-            self.stdout.write("Rendering %s template files with "
-                              "filenames: %s\n" %
-                              (app_or_project, ', '.join(extra_files)))
+            self.info("Rendering %s template files with extensions: %s" %
+                      (app_or_project, ', '.join(extensions)))
+            self.info("Rendering %s template files with filenames: %s" %
+                      (app_or_project, ', '.join(extra_files)))
 
         base_name = '%s_name' % app_or_project
         base_subdir = '%s_template' % app_or_project
@@ -161,7 +159,7 @@ class TemplateCommand(BaseCommand):
                     new_file.write(content)
 
                 if self.verbosity >= 2:
-                    self.stdout.write("Creating %s\n" % new_path)
+                    self.info("Creating %s" % new_path)
                 try:
                     shutil.copymode(old_path, new_path)
                     self.make_writeable(new_path)
@@ -169,12 +167,12 @@ class TemplateCommand(BaseCommand):
                     notice = self.style.NOTICE(
                         "Notice: Couldn't set permission bits on %s. You're "
                         "probably using an uncommon filesystem setup. No "
-                        "problem.\n" % new_path)
-                    sys.stderr.write(smart_str(notice))
+                        "problem." % new_path)
+                    self.error(notice)
 
         if self.paths_to_remove:
             if self.verbosity >= 2:
-                self.stdout.write("Cleaning up temporary files.\n")
+                self.info("Cleaning up temporary files.")
             for path_to_remove in self.paths_to_remove:
                 if path.isfile(path_to_remove):
                     os.remove(path_to_remove)
@@ -227,7 +225,7 @@ class TemplateCommand(BaseCommand):
         filename, display_url = cleanup_url(url)
 
         if self.verbosity >= 2:
-            self.stdout.write("Downloading %s\n" % display_url)
+            self.info("Downloading %s" % display_url)
         try:
             the_path, info = urllib.urlretrieve(url,
                                                 path.join(tempdir, filename))
@@ -282,7 +280,7 @@ class TemplateCommand(BaseCommand):
         tempdir = tempfile.mkdtemp(prefix=prefix, suffix='_extract')
         self.paths_to_remove.append(tempdir)
         if self.verbosity >= 2:
-            self.stdout.write("Extracting %s\n" % filename)
+            self.info("Extracting %s" % filename)
         try:
             archive.extract(filename, tempdir)
             return tempdir
diff --git a/django/utils/log.py b/django/utils/log.py
index df2089f..6ffa871 100644
--- a/django/utils/log.py
+++ b/django/utils/log.py
@@ -1,4 +1,5 @@
 import logging
+import sys
 import traceback
 
 from django.conf import settings
@@ -21,15 +22,6 @@ try:
 except ImportError:
     from django.utils.dictconfig import dictConfig
 
-getLogger = logging.getLogger
-
-# Ensure the creation of the Django logger
-# with a null handler. This ensures we don't get any
-# 'No handlers could be found for logger "django"' messages
-logger = getLogger('django')
-if not logger.handlers:
-    logger.addHandler(NullHandler())
-
 
 class AdminEmailHandler(logging.Handler):
     """An exception log handler that emails log entries to site admins.
@@ -103,3 +95,40 @@ class CallbackFilter(logging.Filter):
 class RequireDebugFalse(logging.Filter):
     def filter(self, record):
         return not settings.DEBUG
+
+
+class SingleLevelFilter(logging.Filter):
+    def __init__(self, passlevel, reject):
+        self.passlevel = passlevel
+        self.reject = reject
+
+    def filter(self, record):
+        if self.reject:
+            return (record.levelno != self.passlevel)
+        else:
+            return (record.levelno == self.passlevel)
+
+
+def setup_commands_logger(name, stdout, stderr):
+    logger = getLogger(name)
+    if not logger.handlers or name.endswith('temp'):
+        logger.setLevel(logging.DEBUG)
+        logger.handlers = []
+        handler_out = logging.StreamHandler(stdout)
+        handler_out.addFilter(SingleLevelFilter(logging.INFO, False))
+        handler_err = logging.StreamHandler(stderr)
+        handler_err.addFilter(SingleLevelFilter(logging.INFO, True))
+        logger.addHandler(handler_out)
+        logger.addHandler(handler_err)
+    return logger
+
+getLogger = logging.getLogger
+
+# Ensure the creation of the Django logger
+# with a null handler. This ensures we don't get any
+# 'No handlers could be found for logger "django"' messages
+logger = getLogger('django')
+if not logger.handlers:
+    logger.addHandler(NullHandler())
+
+setup_commands_logger('django.commands', sys.stdout, sys.stderr)
diff --git a/docs/howto/custom-management-commands.txt b/docs/howto/custom-management-commands.txt
index ba8765b..30c57e5 100644
--- a/docs/howto/custom-management-commands.txt
+++ b/docs/howto/custom-management-commands.txt
@@ -61,14 +61,14 @@ look like this:
                 poll.opened = False
                 poll.save()
 
-                self.stdout.write('Successfully closed poll "%s"\n' % poll_id)
+                self.info('Successfully closed poll "%s"' % poll_id)
 
 .. note::
     When you are using management commands and wish to provide console
-    output, you should write to ``self.stdout`` and ``self.stderr``,
-    instead of printing to ``stdout`` and ``stderr`` directly. By
-    using these proxies, it becomes much easier to test your custom
-    command.
+    output, you should use the :meth:`~BaseCommand.info` or
+    :meth:`~BaseCommand.error` methods, instead of printing to ``stdout``
+    and ``stderr`` directly. By using these proxies, it becomes much easier
+    to test your custom command or redirect output when needed.
 
 The new custom command can be called using ``python manage.py closepoll
 <poll_id>``.
@@ -261,6 +261,16 @@ the :meth:`~BaseCommand.handle` method must be implemented.
 
     The actual logic of the command. Subclasses must implement this method.
 
+.. method:: BaseCommand.info(message)
+
+    Output a message on the console (stdout), unless the default
+    'django.commands' logger has been customized.
+
+.. method:: BaseCommand.error(message)
+
+    Output an error message on the console (stderr), unless the default
+    'django.commands' logger has been customized.
+
 .. _ref-basecommand-subclasses:
 
 BaseCommand subclasses
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index f3ce5f8..7ddc368 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -276,6 +276,9 @@ these changes.
 * The function ``django.utils.itercompat.product`` will be removed. The Python
   builtin version should be used instead.
 
+* The self.stdout and self.stderr attributes of management BaseCommand will be
+  removed. They are replaced by the info() and error() methods, added in 1.5.
+
 2.0
 ---
 
diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
index 51766dc..6425a03 100644
--- a/docs/releases/1.4.txt
+++ b/docs/releases/1.4.txt
@@ -670,6 +670,7 @@ Django 1.4 also includes several smaller improvements worth noting:
   useful in tests that don't need to hit a database. See
   :ref:`testcase_hierarchy_diagram`.
 
+
 Backwards incompatible changes in 1.4
 =====================================
 
diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt
index 5a92e7d..f86ad12 100644
--- a/docs/releases/1.5.txt
+++ b/docs/releases/1.5.txt
@@ -44,6 +44,10 @@ Django 1.5 also includes several smaller improvements worth noting:
 * :mod:`django.utils.timezone` provides a helper for converting aware
   datetimes between time zones. See :func:`~django.utils.timezone.localtime`.
 
+* Management commands have now ``info()`` and ``error()`` methods for console
+  output. Output directed to those methods can also be configured by customizing
+  the 'django.commands' logger.
+
 Backwards incompatible changes in 1.5
 =====================================
 
diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt
index aa2afba..4ee2d9f 100644
--- a/docs/topics/logging.txt
+++ b/docs/topics/logging.txt
@@ -455,6 +455,12 @@ For performance reasons, SQL logging is only enabled when
 ``settings.DEBUG`` is set to ``True``, regardless of the logging
 level or handlers that are installed.
 
+``django.commands``
+~~~~~~~~~~~~~~~~~~~
+
+Messages related to management commands. By default, this logger outputs
+message on standard output (console).
+
 Handlers
 --------
 
diff --git a/tests/modeltests/fixtures/tests.py b/tests/modeltests/fixtures/tests.py
index d22010d..7174cea 100644
--- a/tests/modeltests/fixtures/tests.py
+++ b/tests/modeltests/fixtures/tests.py
@@ -185,18 +185,14 @@ class FixtureLoadingTests(TestCase):
             exclude_list=['fixtures.Article', 'fixtures.Book', 'sites'])
 
         # Excluding a bogus app should throw an error
-        self.assertRaises(SystemExit,
-                          self._dumpdata_assert,
-                          ['fixtures', 'sites'],
-                          '',
-                          exclude_list=['foo_app'])
+        with self.assertRaisesRegexp(management.CommandError,
+                                     "Unknown app in excludes: foo_app"):
+            self._dumpdata_assert(['fixtures', 'sites'], '', exclude_list=['foo_app'])
 
         # Excluding a bogus model should throw an error
-        self.assertRaises(SystemExit,
-                          self._dumpdata_assert,
-                          ['fixtures', 'sites'],
-                          '',
-                          exclude_list=['fixtures.FooModel'])
+        with self.assertRaisesRegexp(management.CommandError,
+                                     "Unknown model in excludes: fixtures.FooModel"):
+            self._dumpdata_assert(['fixtures', 'sites'], '', exclude_list=['fixtures.FooModel'])
 
     def test_dumpdata_with_filtering_manager(self):
         spy1 = Spy.objects.create(name='Paul')
@@ -233,10 +229,10 @@ class FixtureLoadingTests(TestCase):
     def test_ambiguous_compressed_fixture(self):
         # The name "fixture5" is ambigous, so loading it will raise an error
         new_io = StringIO.StringIO()
-        management.call_command('loaddata', 'fixture5', verbosity=0, stderr=new_io, commit=False)
-        output = new_io.getvalue().strip().split('\n')
-        self.assertEqual(len(output), 1)
-        self.assertTrue(output[0].startswith("Multiple fixtures named 'fixture5'"))
+        with self.assertRaisesRegexp(management.CommandError,
+                                     "Multiple fixtures named 'fixture5'"):
+            management.call_command('loaddata', 'fixture5', verbosity=0,
+                                    stderr=new_io, commit=False)
 
     def test_db_loading(self):
         # Load db fixtures 1 and 2. These will load using the 'default' database identifier implicitly
@@ -261,7 +257,8 @@ class FixtureLoadingTests(TestCase):
         new_io = StringIO.StringIO()
         management.call_command('loaddata', 'invalid.json', verbosity=0, stderr=new_io, commit=False)
         output = new_io.getvalue().strip().split('\n')
-        self.assertRegexpMatches(output[-1], "Error: Could not load fixtures.Article\(pk=1\): .*$")
+        # Last lines may be polluted by error formatting
+        self.assertIn("Error: Could not load fixtures.Article(pk=1):", "".join(output[-3:]))
 
     def test_loading_using(self):
         # Load db fixtures 1 and 2. These will load using the 'default' database identifier explicitly
@@ -317,10 +314,9 @@ class FixtureTransactionTests(TransactionTestCase):
         # Try to load fixture 2 using format discovery; this will fail
         # because there are two fixture2's in the fixtures directory
         new_io = StringIO.StringIO()
-        management.call_command('loaddata', 'fixture2', verbosity=0, stderr=new_io)
-        output = new_io.getvalue().strip().split('\n')
-        self.assertEqual(len(output), 1)
-        self.assertTrue(output[0].startswith("Multiple fixtures named 'fixture2'"))
+        with self.assertRaisesRegexp(management.CommandError,
+                                     "Multiple fixtures named 'fixture2'"):
+            management.call_command('loaddata', 'fixture2', verbosity=0, stderr=new_io)
 
         # object list is unaffected
         self.assertQuerysetEqual(Article.objects.all(), [
diff --git a/tests/modeltests/user_commands/management/commands/dance.py b/tests/modeltests/user_commands/management/commands/dance.py
index 4ad5579..47875d2 100644
--- a/tests/modeltests/user_commands/management/commands/dance.py
+++ b/tests/modeltests/user_commands/management/commands/dance.py
@@ -15,4 +15,4 @@ class Command(BaseCommand):
 
     def handle(self, *args, **options):
         example = options["example"]
-        self.stdout.write("I don't feel like dancing %s." % options["style"])
+        self.info("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 896dd66..c1e2bf9 100644
--- a/tests/modeltests/user_commands/tests.py
+++ b/tests/modeltests/user_commands/tests.py
@@ -11,13 +11,13 @@ class CommandTests(TestCase):
         out = StringIO()
         management.call_command('dance', stdout=out)
         self.assertEqual(out.getvalue(),
-            "I don't feel like dancing Rock'n'Roll.")
+            "I don't feel like dancing Rock'n'Roll.\n")
 
     def test_command_style(self):
         out = StringIO()
         management.call_command('dance', style='Jive', stdout=out)
         self.assertEqual(out.getvalue(),
-            "I don't feel like dancing Jive.")
+            "I don't feel like dancing Jive.\n")
 
     def test_language_preserved(self):
         out = StringIO()
diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py
index 780718b..d8a93cc 100644
--- a/tests/regressiontests/cache/tests.py
+++ b/tests/regressiontests/cache/tests.py
@@ -820,9 +820,14 @@ class DBCacheTests(BaseCacheTests, TransactionTestCase):
         self.perform_cull_test(50, 18)
 
     def test_second_call_doesnt_crash(self):
-        err = StringIO.StringIO()
-        management.call_command('createcachetable', self._table_name, verbosity=0, interactive=False, stderr=err)
-        self.assertTrue("Cache table 'test cache table' could not be created" in err.getvalue())
+        with self.assertRaisesRegexp(management.CommandError,
+            "Cache table 'test cache table' could not be created"):
+            management.call_command(
+                'createcachetable',
+                self._table_name,
+                verbosity=0,
+                interactive=False
+            )
 
 
 @override_settings(USE_TZ=True)
diff --git a/tests/regressiontests/fixtures_regress/tests.py b/tests/regressiontests/fixtures_regress/tests.py
index ed8b404..5c1a70e 100644
--- a/tests/regressiontests/fixtures_regress/tests.py
+++ b/tests/regressiontests/fixtures_regress/tests.py
@@ -119,18 +119,14 @@ class TestFixtures(TestCase):
         Test for ticket #4371 -- Loading data of an unknown format should fail
         Validate that error conditions are caught correctly
         """
-        stderr = StringIO()
-        management.call_command(
-            'loaddata',
-            'bad_fixture1.unkn',
-            verbosity=0,
-            commit=False,
-            stderr=stderr,
-        )
-        self.assertEqual(
-            stderr.getvalue(),
-            "Problem installing fixture 'bad_fixture1': unkn is not a known serialization format.\n"
-        )
+        with self.assertRaisesRegexp(CommandError,
+            "Problem installing fixture 'bad_fixture1': unkn is not a known serialization format."):
+            management.call_command(
+                'loaddata',
+                'bad_fixture1.unkn',
+                verbosity=0,
+                commit=False,
+            )
 
     def test_invalid_data(self):
         """
@@ -138,18 +134,14 @@ class TestFixtures(TestCase):
         using explicit filename.
         Validate that error conditions are caught correctly
         """
-        stderr = StringIO()
-        management.call_command(
-            'loaddata',
-            'bad_fixture2.xml',
-            verbosity=0,
-            commit=False,
-            stderr=stderr,
-        )
-        self.assertEqual(
-            stderr.getvalue(),
-            "No fixture data found for 'bad_fixture2'. (File format may be invalid.)\n"
-        )
+        with self.assertRaisesRegexp(CommandError,
+            "No fixture data found for 'bad_fixture2'. \(File format may be invalid.\)"):
+            management.call_command(
+                'loaddata',
+                'bad_fixture2.xml',
+                verbosity=0,
+                commit=False,
+            )
 
     def test_invalid_data_no_ext(self):
         """
@@ -157,73 +149,42 @@ class TestFixtures(TestCase):
         without file extension.
         Validate that error conditions are caught correctly
         """
-        stderr = StringIO()
-        management.call_command(
-            'loaddata',
-            'bad_fixture2',
-            verbosity=0,
-            commit=False,
-            stderr=stderr,
-        )
-        self.assertEqual(
-            stderr.getvalue(),
-            "No fixture data found for 'bad_fixture2'. (File format may be invalid.)\n"
-        )
+        with self.assertRaisesRegexp(CommandError,
+            "No fixture data found for 'bad_fixture2'. \(File format may be invalid.\)"):
+            management.call_command(
+                'loaddata',
+                'bad_fixture2',
+                verbosity=0,
+                commit=False,
+            )
 
     def test_empty(self):
         """
         Test for ticket #4371 -- Loading a fixture file with no data returns an error.
         Validate that error conditions are caught correctly
         """
-        stderr = StringIO()
-        management.call_command(
-            'loaddata',
-            'empty',
-            verbosity=0,
-            commit=False,
-            stderr=stderr,
-        )
-        self.assertEqual(
-            stderr.getvalue(),
-            "No fixture data found for 'empty'. (File format may be invalid.)\n"
-        )
-
-    def test_abort_loaddata_on_error(self):
-        """
-        Test for ticket #4371 -- If any of the fixtures contain an error,
-        loading is aborted.
-        Validate that error conditions are caught correctly
-        """
-        stderr = StringIO()
-        management.call_command(
-            'loaddata',
-            'empty',
-            verbosity=0,
-            commit=False,
-            stderr=stderr,
-        )
-        self.assertEqual(
-            stderr.getvalue(),
-            "No fixture data found for 'empty'. (File format may be invalid.)\n"
-        )
+        with self.assertRaisesRegexp(CommandError,
+            "No fixture data found for 'empty'. \(File format may be invalid.\)"):
+            management.call_command(
+                'loaddata',
+                'empty',
+                verbosity=0,
+                commit=False,
+            )
 
     def test_error_message(self):
         """
         (Regression for #9011 - error message is correct)
         """
-        stderr = StringIO()
-        management.call_command(
-            'loaddata',
-            'bad_fixture2',
-            'animal',
-            verbosity=0,
-            commit=False,
-            stderr=stderr,
-        )
-        self.assertEqual(
-            stderr.getvalue(),
-            "No fixture data found for 'bad_fixture2'. (File format may be invalid.)\n"
-        )
+        with self.assertRaisesRegexp(CommandError,
+            "No fixture data found for 'bad_fixture2'. \(File format may be invalid.\)"):
+            management.call_command(
+                'loaddata',
+                'bad_fixture2',
+                'animal',
+                verbosity=0,
+                commit=False,
+            )
 
     def test_pg_sequence_resetting_checks(self):
         """
@@ -337,7 +298,8 @@ class TestFixtures(TestCase):
         platypus_json = '{"pk": %d, "model": "fixtures_regress.animal", "fields": {"count": 2, "weight": 2.2, "name": "Platypus", "latin_name": "Ornithorhynchus anatinus"}}'
         platypus_json = platypus_json % animal.pk
 
-        self.assertEqual(len(data), len('[%s]' % ', '.join([lion_json, emu_json, platypus_json])))
+        # -1 is for the final newline
+        self.assertEqual(len(data)-1, len('[%s]' % ', '.join([lion_json, emu_json, platypus_json])))
         self.assertTrue(lion_json in data)
         self.assertTrue(emu_json in data)
         self.assertTrue(platypus_json in data)
@@ -358,7 +320,7 @@ class TestFixtures(TestCase):
         )
         self.assertEqual(
             stdout.getvalue(),
-            """[{"pk": %d, "model": "fixtures_regress.widget", "fields": {"name": "grommet"}}]"""
+            """[{"pk": %d, "model": "fixtures_regress.widget", "fields": {"name": "grommet"}}]\n"""
             % widget.pk
             )
 
@@ -387,9 +349,7 @@ class TestFixtures(TestCase):
             commit=False,
             stderr=stderr,
         )
-        self.assertTrue(
-            stderr.getvalue().startswith('Problem installing fixture')
-        )
+        self.assertIn('Problem installing fixture', stderr.getvalue())
 
     _cur_dir = os.path.dirname(os.path.abspath(__file__))
 
@@ -414,16 +374,14 @@ class TestFixtures(TestCase):
         """
         Regression for #7043 - Error is quickly reported when no fixtures is provided in the command line.
         """
-        stderr = StringIO()
-        management.call_command(
-            'loaddata',
-            verbosity=0,
-            commit=False,
-            stderr=stderr,
-        )
-        self.assertEqual(
-            stderr.getvalue(), 'No database fixture specified. Please provide the path of at least one fixture in the command line.\n'
-        )
+        with self.assertRaisesRegexp(CommandError,
+            "No database fixture specified. "
+            "Please provide the path of at least one fixture in the command line."):
+            management.call_command(
+                'loaddata',
+                verbosity=0,
+                commit=False,
+            )
 
     def test_loaddata_not_existant_fixture_file(self):
         stdout_output = StringIO()
@@ -524,7 +482,7 @@ class NaturalKeyFixtureTests(TestCase):
         )
         self.assertEqual(
             stdout.getvalue(),
-            """[{"pk": 2, "model": "fixtures_regress.store", "fields": {"name": "Amazon"}}, {"pk": 3, "model": "fixtures_regress.store", "fields": {"name": "Borders"}}, {"pk": 4, "model": "fixtures_regress.person", "fields": {"name": "Neal Stephenson"}}, {"pk": 1, "model": "fixtures_regress.book", "fields": {"stores": [["Amazon"], ["Borders"]], "name": "Cryptonomicon", "author": ["Neal Stephenson"]}}]"""
+            """[{"pk": 2, "model": "fixtures_regress.store", "fields": {"name": "Amazon"}}, {"pk": 3, "model": "fixtures_regress.store", "fields": {"name": "Borders"}}, {"pk": 4, "model": "fixtures_regress.person", "fields": {"name": "Neal Stephenson"}}, {"pk": 1, "model": "fixtures_regress.book", "fields": {"stores": [["Amazon"], ["Borders"]], "name": "Cryptonomicon", "author": ["Neal Stephenson"]}}]\n"""
         )
 
     def test_dependency_sorting(self):
diff --git a/tests/regressiontests/i18n/commands/compilation.py b/tests/regressiontests/i18n/commands/compilation.py
index 1f38910..2cd02d5 100644
--- a/tests/regressiontests/i18n/commands/compilation.py
+++ b/tests/regressiontests/i18n/commands/compilation.py
@@ -1,17 +1,14 @@
 import os
-try:
-    from cStringIO import StringIO
-except ImportError:
-    from StringIO import StringIO
+from StringIO import StringIO
 
-from django.core.management import CommandError
-from django.core.management.commands.compilemessages import compile_messages
+from django.core.management import call_command, CommandError
 from django.test import TestCase
 from django.test.utils import override_settings
 from django.utils import translation
 
 test_dir = os.path.abspath(os.path.dirname(__file__))
 
+
 class MessageCompilationTests(TestCase):
 
     def setUp(self):
@@ -28,11 +25,9 @@ class PoFileTests(MessageCompilationTests):
 
     def test_bom_rejection(self):
         os.chdir(test_dir)
-        # We don't use the django.core.management infrastructure (call_command()
-        # et al) because CommandError's cause exit(1) there. We test the
-        # underlying compile_messages function instead
         out = StringIO()
-        self.assertRaises(CommandError, compile_messages, out, locale=self.LOCALE)
+        self.assertRaises(CommandError, call_command, 'compilemessages',
+                          locale=self.LOCALE, stdout=out, stderr=out)
         self.assertFalse(os.path.exists(self.MO_FILE))
 
 
@@ -48,11 +43,7 @@ class PoFileContentsTests(MessageCompilationTests):
 
     def test_percent_symbol_in_po_file(self):
         os.chdir(test_dir)
-        # We don't use the django.core.management infrastructure (call_command()
-        # et al) because CommandError's cause exit(1) there. We test the
-        # underlying compile_messages function instead
-        out = StringIO()
-        compile_messages(out, locale=self.LOCALE)
+        call_command('compilemessages', locale=self.LOCALE, stdout=StringIO())
         self.assertTrue(os.path.exists(self.MO_FILE))
 
 
@@ -67,11 +58,7 @@ class PercentRenderingTests(MessageCompilationTests):
     def test_percent_symbol_escaping(self):
         from django.template import Template, Context
         os.chdir(test_dir)
-        # We don't use the django.core.management infrastructure (call_command()
-        # et al) because CommandError's cause exit(1) there. We test the
-        # underlying compile_messages function instead
-        out = StringIO()
-        compile_messages(out, locale=self.LOCALE)
+        call_command('compilemessages', locale=self.LOCALE, stdout=StringIO())
         with translation.override(self.LOCALE):
             t = Template('{% load i18n %}{% trans "Looks like a str fmt spec %% o but shouldn\'t be interpreted as such" %}')
             rendered = t.render(Context({}))
diff --git a/tests/regressiontests/staticfiles_tests/tests.py b/tests/regressiontests/staticfiles_tests/tests.py
index 7f30cb9..31a6c6f 100644
--- a/tests/regressiontests/staticfiles_tests/tests.py
+++ b/tests/regressiontests/staticfiles_tests/tests.py
@@ -187,30 +187,20 @@ class TestFindStatic(CollectionTestCase, TestDefaults):
     Test ``findstatic`` management command.
     """
     def _get_file(self, filepath):
-        _stdout = sys.stdout
-        sys.stdout = StringIO()
-        try:
-            call_command('findstatic', filepath, all=False, verbosity='0')
-            sys.stdout.seek(0)
-            lines = [l.strip() for l in sys.stdout.readlines()]
-            contents = codecs.open(
-                smart_unicode(lines[1].strip()), "r", "utf-8").read()
-        finally:
-            sys.stdout = _stdout
+        out = StringIO()
+        call_command('findstatic', filepath, all=False, verbosity='0', stdout=out)
+        lines = [l.strip() for l in out.getvalue().split('\n') if l != '']
+        contents = codecs.open(
+            smart_unicode(lines[1].strip()), "r", "utf-8").read()
         return contents
 
     def test_all_files(self):
         """
         Test that findstatic returns all candidate files if run without --first.
         """
-        _stdout = sys.stdout
-        sys.stdout = StringIO()
-        try:
-            call_command('findstatic', 'test/file.txt', verbosity='0')
-            sys.stdout.seek(0)
-            lines = [l.strip() for l in sys.stdout.readlines()]
-        finally:
-            sys.stdout = _stdout
+        out = StringIO()
+        call_command('findstatic', 'test/file.txt', verbosity='0', stdout=out)
+        lines = [l.strip() for l in out.getvalue().split('\n') if l != '']
         self.assertEqual(len(lines), 3)  # three because there is also the "Found <file> here" line
         self.assertIn('project', lines[1])
         self.assertIn('apps', lines[2])
