Browse Source

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

Hasan Ramezani 6 years ago
parent
commit
5195b99e2c

+ 14 - 4
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'):

+ 2 - 2
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', ''))

+ 9 - 1
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`_

+ 2 - 1
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
 ~~~~~~~~~~

+ 70 - 33
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
         )
 
 

+ 6 - 6
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)