diff --git a/django/core/management/base.py b/django/core/management/base.py index 651674534f..354e778a70 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -95,7 +95,7 @@ class DjangoHelpFormatter(HelpFormatter): """ show_last = { '--version', '--verbosity', '--traceback', '--settings', '--pythonpath', - '--no-color', + '--no-color', '--force_color', } def _reordered_actions(self, actions): @@ -227,13 +227,15 @@ class BaseCommand: # Command-specific options not defined by the argument parser. stealth_options = () - def __init__(self, stdout=None, stderr=None, no_color=False): + def __init__(self, stdout=None, stderr=None, no_color=False, force_color=False): self.stdout = OutputWrapper(stdout or sys.stdout) self.stderr = OutputWrapper(stderr or sys.stderr) + if no_color and force_color: + raise CommandError("'no_color' and 'force_color' can't be used together.") if no_color: self.style = no_style() else: - self.style = color_style() + self.style = color_style(force_color) self.stderr.style_func = self.style.ERROR def get_version(self): @@ -280,6 +282,10 @@ class BaseCommand: '--no-color', action='store_true', help="Don't colorize the command output.", ) + parser.add_argument( + '--force-color', action='store_true', + help='Force colorization of the command output.', + ) self.add_arguments(parser) return parser @@ -339,7 +345,11 @@ class BaseCommand: controlled by the ``requires_system_checks`` attribute, except if force-skipped). """ - if options['no_color']: + if options['force_color'] and options['no_color']: + raise CommandError("The --no-color and --force-color options can't be used together.") + if options['force_color']: + self.style = color_style(force_color=True) + elif options['no_color']: self.style = no_style() self.stderr.style_func = None if options.get('stdout'): diff --git a/django/core/management/color.py b/django/core/management/color.py index 42600fa1c8..572329bb0c 100644 --- a/django/core/management/color.py +++ b/django/core/management/color.py @@ -64,10 +64,10 @@ def no_style(): return make_style('nocolor') -def color_style(): +def color_style(force_color=False): """ Return a Style object from the Django color scheme. """ - if not supports_color(): + if not force_color and not supports_color(): return no_style() return make_style(os.environ.get('DJANGO_COLORS', '')) diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 408c39055e..f1ebc24ee6 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -1657,6 +1657,14 @@ Example usage:: django-admin runserver --no-color +.. django-admin-option:: --force-color + +.. versionadded:: 2.2 + +Forces colorization of the command output if it would otherwise be disabled +as discussed in :ref:`syntax-coloring`. For example, you may want to pipe +colored output to another command. + Extra niceties ============== @@ -1668,7 +1676,7 @@ Syntax coloring The ``django-admin`` / ``manage.py`` commands will use pretty color-coded output if your terminal supports ANSI-colored output. It won't use the color codes if you're piping the command's output to -another program. +another program unless the :option:`--force-color` option is used. Under Windows, the native console doesn't support ANSI escape sequences so by default there is no color output. But you can install the `ANSICON`_ diff --git a/docs/releases/2.2.txt b/docs/releases/2.2.txt index 2709344de0..af2910a514 100644 --- a/docs/releases/2.2.txt +++ b/docs/releases/2.2.txt @@ -170,7 +170,8 @@ Internationalization Management Commands ~~~~~~~~~~~~~~~~~~~ -* ... +* The new :option:`--force-color` option forces colorization of the command + output. Migrations ~~~~~~~~~~ diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index df0fcd6276..410652efbc 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -40,7 +40,7 @@ custom_templates_dir = os.path.join(os.path.dirname(__file__), 'custom_templates SYSTEM_CHECK_MSG = 'System check identified no issues' -class AdminScriptTestCase(unittest.TestCase): +class AdminScriptTestCase(SimpleTestCase): @classmethod def setUpClass(cls): @@ -970,9 +970,9 @@ class ManageAlternateSettings(AdminScriptTestCase): out, err = self.run_manage(args) self.assertOutput( out, - "EXECUTE: noargs_command options=[('no_color', False), " - "('pythonpath', None), ('settings', 'alternate_settings'), " - "('traceback', False), ('verbosity', 1)]" + "EXECUTE: noargs_command options=[('force_color', False), " + "('no_color', False), ('pythonpath', None), ('settings', " + "'alternate_settings'), ('traceback', False), ('verbosity', 1)]" ) self.assertNoOutput(err) @@ -982,9 +982,9 @@ class ManageAlternateSettings(AdminScriptTestCase): out, err = self.run_manage(args, 'alternate_settings') self.assertOutput( out, - "EXECUTE: noargs_command options=[('no_color', False), " - "('pythonpath', None), ('settings', None), ('traceback', False), " - "('verbosity', 1)]" + "EXECUTE: noargs_command options=[('force_color', False), " + "('no_color', False), ('pythonpath', None), ('settings', None), " + "('traceback', False), ('verbosity', 1)]" ) self.assertNoOutput(err) @@ -994,9 +994,9 @@ class ManageAlternateSettings(AdminScriptTestCase): out, err = self.run_manage(args) self.assertOutput( out, - "EXECUTE: noargs_command options=[('no_color', True), " - "('pythonpath', None), ('settings', 'alternate_settings'), " - "('traceback', False), ('verbosity', 1)]" + "EXECUTE: noargs_command options=[('force_color', False), " + "('no_color', True), ('pythonpath', None), ('settings', " + "'alternate_settings'), ('traceback', False), ('verbosity', 1)]" ) self.assertNoOutput(err) @@ -1425,7 +1425,7 @@ class ManageTestserver(AdminScriptTestCase): 'blah.json', stdout=out, settings=None, pythonpath=None, verbosity=1, traceback=False, addrport='', no_color=False, use_ipv6=False, - skip_checks=True, interactive=True, + skip_checks=True, interactive=True, force_color=False, ) @mock.patch('django.db.connection.creation.create_test_db', return_value='test_db') @@ -1436,6 +1436,7 @@ class ManageTestserver(AdminScriptTestCase): call_command('testserver', 'blah.json', stdout=out) mock_runserver_handle.assert_called_with( addrport='', + force_color=False, insecure_serving=False, no_color=False, pythonpath=None, @@ -1578,6 +1579,34 @@ class CommandTypes(AdminScriptTestCase): self.assertEqual(out.getvalue(), 'Hello, world!\n') self.assertEqual(err.getvalue(), 'Hello, world!\n') + def test_force_color_execute(self): + out = StringIO() + err = StringIO() + with mock.patch.object(sys.stdout, 'isatty', lambda: False): + command = ColorCommand(stdout=out, stderr=err) + call_command(command, force_color=True) + self.assertEqual(out.getvalue(), '\x1b[31;1mHello, world!\n\x1b[0m') + self.assertEqual(err.getvalue(), '\x1b[31;1mHello, world!\n\x1b[0m') + + def test_force_color_command_init(self): + out = StringIO() + err = StringIO() + with mock.patch.object(sys.stdout, 'isatty', lambda: False): + command = ColorCommand(stdout=out, stderr=err, force_color=True) + call_command(command) + self.assertEqual(out.getvalue(), '\x1b[31;1mHello, world!\n\x1b[0m') + self.assertEqual(err.getvalue(), '\x1b[31;1mHello, world!\n\x1b[0m') + + def test_no_color_force_color_mutually_exclusive_execute(self): + msg = "The --no-color and --force-color options can't be used together." + with self.assertRaisesMessage(CommandError, msg): + call_command(BaseCommand(), no_color=True, force_color=True) + + def test_no_color_force_color_mutually_exclusive_command_init(self): + msg = "'no_color' and 'force_color' can't be used together." + with self.assertRaisesMessage(CommandError, msg): + call_command(BaseCommand(no_color=True, force_color=True)) + def test_custom_stdout(self): class Command(BaseCommand): requires_system_checks = False @@ -1655,9 +1684,10 @@ class CommandTypes(AdminScriptTestCase): expected_out = ( "EXECUTE:BaseCommand labels=%s, " - "options=[('no_color', False), ('option_a', %s), ('option_b', %s), " - "('option_c', '3'), ('pythonpath', None), ('settings', None), " - "('traceback', False), ('verbosity', 1)]") % (labels, option_a, option_b) + "options=[('force_color', False), ('no_color', False), " + "('option_a', %s), ('option_b', %s), ('option_c', '3'), " + "('pythonpath', None), ('settings', None), ('traceback', False), " + "('verbosity', 1)]") % (labels, option_a, option_b) self.assertNoOutput(err) self.assertOutput(out, expected_out) @@ -1731,9 +1761,9 @@ class CommandTypes(AdminScriptTestCase): self.assertNoOutput(err) self.assertOutput( out, - "EXECUTE: noargs_command options=[('no_color', False), " - "('pythonpath', None), ('settings', None), ('traceback', False), " - "('verbosity', 1)]" + "EXECUTE: noargs_command options=[('force_color', False), " + "('no_color', False), ('pythonpath', None), ('settings', None), " + "('traceback', False), ('verbosity', 1)]" ) def test_noargs_with_args(self): @@ -1750,8 +1780,9 @@ class CommandTypes(AdminScriptTestCase): self.assertOutput(out, "EXECUTE:AppCommand name=django.contrib.auth, options=") self.assertOutput( out, - ", options=[('no_color', False), ('pythonpath', None), " - "('settings', None), ('traceback', False), ('verbosity', 1)]" + ", options=[('force_color', False), ('no_color', False), " + "('pythonpath', None), ('settings', None), ('traceback', False), " + "('verbosity', 1)]" ) def test_app_command_no_apps(self): @@ -1768,14 +1799,16 @@ class CommandTypes(AdminScriptTestCase): self.assertOutput(out, "EXECUTE:AppCommand name=django.contrib.auth, options=") self.assertOutput( out, - ", options=[('no_color', False), ('pythonpath', None), " - "('settings', None), ('traceback', False), ('verbosity', 1)]" + ", options=[('force_color', False), ('no_color', False), " + "('pythonpath', None), ('settings', None), ('traceback', False), " + "('verbosity', 1)]" ) self.assertOutput(out, "EXECUTE:AppCommand name=django.contrib.contenttypes, options=") self.assertOutput( out, - ", options=[('no_color', False), ('pythonpath', None), " - "('settings', None), ('traceback', False), ('verbosity', 1)]" + ", options=[('force_color', False), ('no_color', False), " + "('pythonpath', None), ('settings', None), ('traceback', False), " + "('verbosity', 1)]" ) def test_app_command_invalid_app_label(self): @@ -1797,8 +1830,9 @@ class CommandTypes(AdminScriptTestCase): self.assertNoOutput(err) self.assertOutput( out, - "EXECUTE:LabelCommand label=testlabel, options=[('no_color', False), " - "('pythonpath', None), ('settings', None), ('traceback', False), ('verbosity', 1)]" + "EXECUTE:LabelCommand label=testlabel, options=[('force_color', " + "False), ('no_color', False), ('pythonpath', None), ('settings', " + "None), ('traceback', False), ('verbosity', 1)]" ) def test_label_command_no_label(self): @@ -1814,13 +1848,15 @@ class CommandTypes(AdminScriptTestCase): self.assertNoOutput(err) self.assertOutput( out, - "EXECUTE:LabelCommand label=testlabel, options=[('no_color', False), " - "('pythonpath', None), ('settings', None), ('traceback', False), ('verbosity', 1)]" + "EXECUTE:LabelCommand label=testlabel, options=[('force_color', " + "False), ('no_color', False), ('pythonpath', None), " + "('settings', None), ('traceback', False), ('verbosity', 1)]" ) self.assertOutput( out, - "EXECUTE:LabelCommand label=anotherlabel, options=[('no_color', False), " - "('pythonpath', None), ('settings', None), ('traceback', False), ('verbosity', 1)]" + "EXECUTE:LabelCommand label=anotherlabel, options=[('force_color', " + "False), ('no_color', False), ('pythonpath', None), " + "('settings', None), ('traceback', False), ('verbosity', 1)]" ) @@ -1894,10 +1930,11 @@ class ArgumentOrder(AdminScriptTestCase): self.assertNoOutput(err) self.assertOutput( out, - "EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), " - "('option_a', 'x'), ('option_b', %s), ('option_c', '3'), " - "('pythonpath', None), ('settings', 'alternate_settings'), " - "('traceback', False), ('verbosity', 1)]" % option_b + "EXECUTE:BaseCommand labels=('testlabel',), options=[" + "('force_color', False), ('no_color', False), ('option_a', 'x'), " + "('option_b', %s), ('option_c', '3'), ('pythonpath', None), " + "('settings', 'alternate_settings'), ('traceback', False), " + "('verbosity', 1)]" % option_b ) diff --git a/tests/user_commands/tests.py b/tests/user_commands/tests.py index 50b1b4244f..45fe0aaf46 100644 --- a/tests/user_commands/tests.py +++ b/tests/user_commands/tests.py @@ -179,18 +179,18 @@ class CommandTests(SimpleTestCase): def test_call_command_unrecognized_option(self): msg = ( 'Unknown option(s) for dance command: unrecognized. Valid options ' - 'are: example, help, integer, no_color, opt_3, option3, ' - 'pythonpath, settings, skip_checks, stderr, stdout, style, ' - 'traceback, verbosity, version.' + 'are: example, force_color, help, integer, no_color, opt_3, ' + 'option3, pythonpath, settings, skip_checks, stderr, stdout, ' + 'style, traceback, verbosity, version.' ) with self.assertRaisesMessage(TypeError, msg): management.call_command('dance', unrecognized=1) msg = ( 'Unknown option(s) for dance command: unrecognized, unrecognized2. ' - 'Valid options are: example, help, integer, no_color, opt_3, ' - 'option3, pythonpath, settings, skip_checks, stderr, stdout, ' - 'style, traceback, verbosity, version.' + 'Valid options are: example, force_color, help, integer, no_color, ' + 'opt_3, option3, pythonpath, settings, skip_checks, stderr, ' + 'stdout, style, traceback, verbosity, version.' ) with self.assertRaisesMessage(TypeError, msg): management.call_command('dance', unrecognized=1, unrecognized2=1)