Fixed #29560 -- Added --force-color management command option.

This commit is contained in:
Hasan Ramezani 2018-07-22 21:41:47 +04:30 committed by Tim Graham
parent de8eb07c7a
commit 5195b99e2c
6 changed files with 103 additions and 47 deletions

View File

@ -95,7 +95,7 @@ class DjangoHelpFormatter(HelpFormatter):
""" """
show_last = { show_last = {
'--version', '--verbosity', '--traceback', '--settings', '--pythonpath', '--version', '--verbosity', '--traceback', '--settings', '--pythonpath',
'--no-color', '--no-color', '--force_color',
} }
def _reordered_actions(self, actions): def _reordered_actions(self, actions):
@ -227,13 +227,15 @@ class BaseCommand:
# Command-specific options not defined by the argument parser. # Command-specific options not defined by the argument parser.
stealth_options = () 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.stdout = OutputWrapper(stdout or sys.stdout)
self.stderr = OutputWrapper(stderr or sys.stderr) 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: if no_color:
self.style = no_style() self.style = no_style()
else: else:
self.style = color_style() self.style = color_style(force_color)
self.stderr.style_func = self.style.ERROR self.stderr.style_func = self.style.ERROR
def get_version(self): def get_version(self):
@ -280,6 +282,10 @@ class BaseCommand:
'--no-color', action='store_true', '--no-color', action='store_true',
help="Don't colorize the command output.", 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) self.add_arguments(parser)
return parser return parser
@ -339,7 +345,11 @@ class BaseCommand:
controlled by the ``requires_system_checks`` attribute, except if controlled by the ``requires_system_checks`` attribute, except if
force-skipped). 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.style = no_style()
self.stderr.style_func = None self.stderr.style_func = None
if options.get('stdout'): if options.get('stdout'):

View File

@ -64,10 +64,10 @@ def no_style():
return make_style('nocolor') return make_style('nocolor')
def color_style(): def color_style(force_color=False):
""" """
Return a Style object from the Django color scheme. 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 no_style()
return make_style(os.environ.get('DJANGO_COLORS', '')) return make_style(os.environ.get('DJANGO_COLORS', ''))

View File

@ -1657,6 +1657,14 @@ Example usage::
django-admin runserver --no-color 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 Extra niceties
============== ==============
@ -1668,7 +1676,7 @@ Syntax coloring
The ``django-admin`` / ``manage.py`` commands will use pretty The ``django-admin`` / ``manage.py`` commands will use pretty
color-coded output if your terminal supports ANSI-colored output. It 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 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 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`_ default there is no color output. But you can install the `ANSICON`_

View File

@ -170,7 +170,8 @@ Internationalization
Management Commands Management Commands
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
* ... * The new :option:`--force-color` option forces colorization of the command
output.
Migrations Migrations
~~~~~~~~~~ ~~~~~~~~~~

View File

@ -40,7 +40,7 @@ custom_templates_dir = os.path.join(os.path.dirname(__file__), 'custom_templates
SYSTEM_CHECK_MSG = 'System check identified no issues' SYSTEM_CHECK_MSG = 'System check identified no issues'
class AdminScriptTestCase(unittest.TestCase): class AdminScriptTestCase(SimpleTestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@ -970,9 +970,9 @@ class ManageAlternateSettings(AdminScriptTestCase):
out, err = self.run_manage(args) out, err = self.run_manage(args)
self.assertOutput( self.assertOutput(
out, out,
"EXECUTE: noargs_command options=[('no_color', False), " "EXECUTE: noargs_command options=[('force_color', False), "
"('pythonpath', None), ('settings', 'alternate_settings'), " "('no_color', False), ('pythonpath', None), ('settings', "
"('traceback', False), ('verbosity', 1)]" "'alternate_settings'), ('traceback', False), ('verbosity', 1)]"
) )
self.assertNoOutput(err) self.assertNoOutput(err)
@ -982,9 +982,9 @@ class ManageAlternateSettings(AdminScriptTestCase):
out, err = self.run_manage(args, 'alternate_settings') out, err = self.run_manage(args, 'alternate_settings')
self.assertOutput( self.assertOutput(
out, out,
"EXECUTE: noargs_command options=[('no_color', False), " "EXECUTE: noargs_command options=[('force_color', False), "
"('pythonpath', None), ('settings', None), ('traceback', False), " "('no_color', False), ('pythonpath', None), ('settings', None), "
"('verbosity', 1)]" "('traceback', False), ('verbosity', 1)]"
) )
self.assertNoOutput(err) self.assertNoOutput(err)
@ -994,9 +994,9 @@ class ManageAlternateSettings(AdminScriptTestCase):
out, err = self.run_manage(args) out, err = self.run_manage(args)
self.assertOutput( self.assertOutput(
out, out,
"EXECUTE: noargs_command options=[('no_color', True), " "EXECUTE: noargs_command options=[('force_color', False), "
"('pythonpath', None), ('settings', 'alternate_settings'), " "('no_color', True), ('pythonpath', None), ('settings', "
"('traceback', False), ('verbosity', 1)]" "'alternate_settings'), ('traceback', False), ('verbosity', 1)]"
) )
self.assertNoOutput(err) self.assertNoOutput(err)
@ -1425,7 +1425,7 @@ class ManageTestserver(AdminScriptTestCase):
'blah.json', 'blah.json',
stdout=out, settings=None, pythonpath=None, verbosity=1, stdout=out, settings=None, pythonpath=None, verbosity=1,
traceback=False, addrport='', no_color=False, use_ipv6=False, 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') @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) call_command('testserver', 'blah.json', stdout=out)
mock_runserver_handle.assert_called_with( mock_runserver_handle.assert_called_with(
addrport='', addrport='',
force_color=False,
insecure_serving=False, insecure_serving=False,
no_color=False, no_color=False,
pythonpath=None, pythonpath=None,
@ -1578,6 +1579,34 @@ class CommandTypes(AdminScriptTestCase):
self.assertEqual(out.getvalue(), 'Hello, world!\n') self.assertEqual(out.getvalue(), 'Hello, world!\n')
self.assertEqual(err.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): def test_custom_stdout(self):
class Command(BaseCommand): class Command(BaseCommand):
requires_system_checks = False requires_system_checks = False
@ -1655,9 +1684,10 @@ class CommandTypes(AdminScriptTestCase):
expected_out = ( expected_out = (
"EXECUTE:BaseCommand labels=%s, " "EXECUTE:BaseCommand labels=%s, "
"options=[('no_color', False), ('option_a', %s), ('option_b', %s), " "options=[('force_color', False), ('no_color', False), "
"('option_c', '3'), ('pythonpath', None), ('settings', None), " "('option_a', %s), ('option_b', %s), ('option_c', '3'), "
"('traceback', False), ('verbosity', 1)]") % (labels, option_a, option_b) "('pythonpath', None), ('settings', None), ('traceback', False), "
"('verbosity', 1)]") % (labels, option_a, option_b)
self.assertNoOutput(err) self.assertNoOutput(err)
self.assertOutput(out, expected_out) self.assertOutput(out, expected_out)
@ -1731,9 +1761,9 @@ class CommandTypes(AdminScriptTestCase):
self.assertNoOutput(err) self.assertNoOutput(err)
self.assertOutput( self.assertOutput(
out, out,
"EXECUTE: noargs_command options=[('no_color', False), " "EXECUTE: noargs_command options=[('force_color', False), "
"('pythonpath', None), ('settings', None), ('traceback', False), " "('no_color', False), ('pythonpath', None), ('settings', None), "
"('verbosity', 1)]" "('traceback', False), ('verbosity', 1)]"
) )
def test_noargs_with_args(self): 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, "EXECUTE:AppCommand name=django.contrib.auth, options=")
self.assertOutput( self.assertOutput(
out, out,
", options=[('no_color', False), ('pythonpath', None), " ", options=[('force_color', False), ('no_color', False), "
"('settings', None), ('traceback', False), ('verbosity', 1)]" "('pythonpath', None), ('settings', None), ('traceback', False), "
"('verbosity', 1)]"
) )
def test_app_command_no_apps(self): 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, "EXECUTE:AppCommand name=django.contrib.auth, options=")
self.assertOutput( self.assertOutput(
out, out,
", options=[('no_color', False), ('pythonpath', None), " ", options=[('force_color', False), ('no_color', False), "
"('settings', None), ('traceback', False), ('verbosity', 1)]" "('pythonpath', None), ('settings', None), ('traceback', False), "
"('verbosity', 1)]"
) )
self.assertOutput(out, "EXECUTE:AppCommand name=django.contrib.contenttypes, options=") self.assertOutput(out, "EXECUTE:AppCommand name=django.contrib.contenttypes, options=")
self.assertOutput( self.assertOutput(
out, out,
", options=[('no_color', False), ('pythonpath', None), " ", options=[('force_color', False), ('no_color', False), "
"('settings', None), ('traceback', False), ('verbosity', 1)]" "('pythonpath', None), ('settings', None), ('traceback', False), "
"('verbosity', 1)]"
) )
def test_app_command_invalid_app_label(self): def test_app_command_invalid_app_label(self):
@ -1797,8 +1830,9 @@ class CommandTypes(AdminScriptTestCase):
self.assertNoOutput(err) self.assertNoOutput(err)
self.assertOutput( self.assertOutput(
out, out,
"EXECUTE:LabelCommand label=testlabel, options=[('no_color', False), " "EXECUTE:LabelCommand label=testlabel, options=[('force_color', "
"('pythonpath', None), ('settings', None), ('traceback', False), ('verbosity', 1)]" "False), ('no_color', False), ('pythonpath', None), ('settings', "
"None), ('traceback', False), ('verbosity', 1)]"
) )
def test_label_command_no_label(self): def test_label_command_no_label(self):
@ -1814,13 +1848,15 @@ class CommandTypes(AdminScriptTestCase):
self.assertNoOutput(err) self.assertNoOutput(err)
self.assertOutput( self.assertOutput(
out, out,
"EXECUTE:LabelCommand label=testlabel, options=[('no_color', False), " "EXECUTE:LabelCommand label=testlabel, options=[('force_color', "
"('pythonpath', None), ('settings', None), ('traceback', False), ('verbosity', 1)]" "False), ('no_color', False), ('pythonpath', None), "
"('settings', None), ('traceback', False), ('verbosity', 1)]"
) )
self.assertOutput( self.assertOutput(
out, out,
"EXECUTE:LabelCommand label=anotherlabel, options=[('no_color', False), " "EXECUTE:LabelCommand label=anotherlabel, options=[('force_color', "
"('pythonpath', None), ('settings', None), ('traceback', False), ('verbosity', 1)]" "False), ('no_color', False), ('pythonpath', None), "
"('settings', None), ('traceback', False), ('verbosity', 1)]"
) )
@ -1894,10 +1930,11 @@ class ArgumentOrder(AdminScriptTestCase):
self.assertNoOutput(err) self.assertNoOutput(err)
self.assertOutput( self.assertOutput(
out, out,
"EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), " "EXECUTE:BaseCommand labels=('testlabel',), options=["
"('option_a', 'x'), ('option_b', %s), ('option_c', '3'), " "('force_color', False), ('no_color', False), ('option_a', 'x'), "
"('pythonpath', None), ('settings', 'alternate_settings'), " "('option_b', %s), ('option_c', '3'), ('pythonpath', None), "
"('traceback', False), ('verbosity', 1)]" % option_b "('settings', 'alternate_settings'), ('traceback', False), "
"('verbosity', 1)]" % option_b
) )

View File

@ -179,18 +179,18 @@ class CommandTests(SimpleTestCase):
def test_call_command_unrecognized_option(self): def test_call_command_unrecognized_option(self):
msg = ( msg = (
'Unknown option(s) for dance command: unrecognized. Valid options ' 'Unknown option(s) for dance command: unrecognized. Valid options '
'are: example, help, integer, no_color, opt_3, option3, ' 'are: example, force_color, help, integer, no_color, opt_3, '
'pythonpath, settings, skip_checks, stderr, stdout, style, ' 'option3, pythonpath, settings, skip_checks, stderr, stdout, '
'traceback, verbosity, version.' 'style, traceback, verbosity, version.'
) )
with self.assertRaisesMessage(TypeError, msg): with self.assertRaisesMessage(TypeError, msg):
management.call_command('dance', unrecognized=1) management.call_command('dance', unrecognized=1)
msg = ( msg = (
'Unknown option(s) for dance command: unrecognized, unrecognized2. ' 'Unknown option(s) for dance command: unrecognized, unrecognized2. '
'Valid options are: example, help, integer, no_color, opt_3, ' 'Valid options are: example, force_color, help, integer, no_color, '
'option3, pythonpath, settings, skip_checks, stderr, stdout, ' 'opt_3, option3, pythonpath, settings, skip_checks, stderr, '
'style, traceback, verbosity, version.' 'stdout, style, traceback, verbosity, version.'
) )
with self.assertRaisesMessage(TypeError, msg): with self.assertRaisesMessage(TypeError, msg):
management.call_command('dance', unrecognized=1, unrecognized2=1) management.call_command('dance', unrecognized=1, unrecognized2=1)