From c38d66a216a45e2352ee666eff9e1270082f906b Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 28 Dec 2009 06:48:47 +0000 Subject: [PATCH] Fixed #12112 -- Made the colors used by syntax highlighting customizable. git-svn-id: http://code.djangoproject.com/svn/django/trunk@12009 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/management/color.py | 26 ++-- django/utils/termcolors.py | 110 +++++++++++++++- docs/ref/django-admin.txt | 87 ++++++++++++- docs/releases/1.2.txt | 11 +- tests/regressiontests/utils/termcolors.py | 149 ++++++++++++++++++++++ tests/regressiontests/utils/tests.py | 3 +- 6 files changed, 368 insertions(+), 18 deletions(-) create mode 100644 tests/regressiontests/utils/termcolors.py diff --git a/django/core/management/color.py b/django/core/management/color.py index f5e39398f45fc..11a9283df9cba 100644 --- a/django/core/management/color.py +++ b/django/core/management/color.py @@ -2,6 +2,7 @@ Sets up the terminal color scheme. """ +import os import sys from django.utils import termcolors @@ -21,16 +22,21 @@ def supports_color(): def color_style(): """Returns a Style object with the Django color scheme.""" if not supports_color(): - return no_style() - class dummy: pass - style = dummy() - style.ERROR = termcolors.make_style(fg='red', opts=('bold',)) - style.ERROR_OUTPUT = termcolors.make_style(fg='red', opts=('bold',)) - style.NOTICE = termcolors.make_style(fg='red') - style.SQL_FIELD = termcolors.make_style(fg='green', opts=('bold',)) - style.SQL_COLTYPE = termcolors.make_style(fg='green') - style.SQL_KEYWORD = termcolors.make_style(fg='yellow') - style.SQL_TABLE = termcolors.make_style(opts=('bold',)) + style = no_style() + else: + DJANGO_COLORS = os.environ.get('DJANGO_COLORS', '') + color_settings = termcolors.parse_color_setting(DJANGO_COLORS) + if color_settings: + class dummy: pass + style = dummy() + # The nocolor palette has all available roles. + # Use that pallete as the basis for populating + # the palette as defined in the environment. + for role in termcolors.PALETTES[termcolors.NOCOLOR_PALETTE]: + format = color_settings.get(role,{}) + setattr(style, role, termcolors.make_style(**format)) + else: + style = no_style() return style def no_style(): diff --git a/django/utils/termcolors.py b/django/utils/termcolors.py index 17a600f8999b5..009b69ed0363e 100644 --- a/django/utils/termcolors.py +++ b/django/utils/termcolors.py @@ -5,7 +5,6 @@ color_names = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white') foreground = dict([(color_names[x], '3%s' % x) for x in range(8)]) background = dict([(color_names[x], '4%s' % x) for x in range(8)]) -del color_names RESET = '0' opt_dict = {'bold': '1', 'underscore': '4', 'blink': '5', 'reverse': '7', 'conceal': '8'} @@ -66,3 +65,112 @@ def make_style(opts=(), **kwargs): COMMENT = make_style(fg='blue', opts=('bold',)) """ return lambda text: colorize(text, opts, **kwargs) + +NOCOLOR_PALETTE = 'nocolor' +DARK_PALETTE = 'dark' +LIGHT_PALETTE = 'light' + +PALETTES = { + NOCOLOR_PALETTE: { + 'ERROR': {}, + 'NOTICE': {}, + 'SQL_FIELD': {}, + 'SQL_COLTYPE': {}, + 'SQL_KEYWORD': {}, + 'SQL_TABLE': {}, + }, + DARK_PALETTE: { + 'ERROR': { 'fg': 'red', 'opts': ('bold',) }, + 'NOTICE': { 'fg': 'red' }, + 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) }, + 'SQL_COLTYPE': { 'fg': 'green' }, + 'SQL_KEYWORD': { 'fg': 'yellow' }, + 'SQL_TABLE': { 'opts': ('bold',) }, + }, + LIGHT_PALETTE: { + 'ERROR': { 'fg': 'red', 'opts': ('bold',) }, + 'NOTICE': { 'fg': 'red' }, + 'SQL_FIELD': { 'fg': 'green', 'opts': ('bold',) }, + 'SQL_COLTYPE': { 'fg': 'green' }, + 'SQL_KEYWORD': { 'fg': 'blue' }, + 'SQL_TABLE': { 'opts': ('bold',) }, + } +} +DEFAULT_PALETTE = DARK_PALETTE + +def parse_color_setting(config_string): + """Parse a DJANGO_COLORS environment variable to produce the system palette + + The general form of a pallete definition is: + + "palette;role=fg;role=fg/bg;role=fg,option,option;role=fg/bg,option,option" + + where: + palette is a named palette; one of 'light', 'dark', or 'nocolor'. + role is a named style used by Django + fg is a background color. + bg is a background color. + option is a display options. + + Specifying a named palette is the same as manually specifying the individual + definitions for each role. Any individual definitions following the pallete + definition will augment the base palette definition. + + Valid roles: + 'error', 'notice', 'sql_field', 'sql_coltype', 'sql_keyword', 'sql_table' + + Valid colors: + 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white' + + Valid options: + 'bold', 'underscore', 'blink', 'reverse', 'conceal' + + """ + if not config_string: + return PALETTES[DEFAULT_PALETTE] + + # Split the color configuration into parts + parts = config_string.lower().split(';') + palette = PALETTES[NOCOLOR_PALETTE].copy() + for part in parts: + if part in PALETTES: + # A default palette has been specified + palette.update(PALETTES[part]) + elif '=' in part: + # Process a palette defining string + definition = {} + + # Break the definition into the role, + # plus the list of specific instructions. + # The role must be in upper case + role, instructions = part.split('=') + role = role.upper() + + styles = instructions.split(',') + styles.reverse() + + # The first instruction can contain a slash + # to break apart fg/bg. + colors = styles.pop().split('/') + colors.reverse() + fg = colors.pop() + if fg in color_names: + definition['fg'] = fg + if colors and colors[-1] in color_names: + definition['bg'] = colors[-1] + + # All remaining instructions are options + opts = tuple(s for s in styles if s in opt_dict.keys()) + if opts: + definition['opts'] = opts + + # The nocolor palette has all available roles. + # Use that palette as the basis for determining + # if the role is valid. + if role in PALETTES[NOCOLOR_PALETTE] and definition: + palette[role] = definition + + # If there are no colors specified, return the empty palette. + if palette == PALETTES[NOCOLOR_PALETTE]: + return None + return palette diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index f5cab244bf910..2cd879423c8d0 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -991,13 +991,92 @@ being executed as an unattended, automated script. Extra niceties ============== +.. _syntax-coloring: + Syntax coloring --------------- -The ``django-admin.py`` / ``manage.py`` commands that output SQL to standard -output 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. +The ``django-admin.py`` / ``manage.py`` commands that output SQL to +standard output 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. + +The colors used for syntax highlighting can be customized. Django +ships with three color palettes: + + * ``dark``, suited to terminals that show white text on a black + background. This is the default palette. + + * ``light``, suited to terminals that show white text on a black + background. + + * ``nocolor``, which disables syntax highlighting. + +You select a palette by setting a ``DJANGO_COLORS`` environment +variable to specify the palette you want to use. For example, to +specify the ``light`` palette under a Unix or OS/X BASH shell, you +would run the following at a command prompt:: + + export DJANGO_COLORS="light" + +You can also customize the colors that are used. Django specifies a +number of roles in which color is used: + + * ``error`` - A major error. + * ``notice`` - A minor error. + * ``sql_field`` - The name of a model field in SQL. + * ``sql_coltype`` - The type of a model field in SQL. + * ``sql_keyword`` - A SQL keyword. + * ``sql_table`` - The name of a model in SQL. + +Each of these roles can be assigned a specific foreground and +background color, from the following list: + + * ``black`` + * ``red`` + * ``green`` + * ``yellow`` + * ``blue`` + * ``magenta`` + * ``cyan`` + * ``white`` + +Each of these colors can then be modified by using the following +display options: + + * ``bold`` + * ``underscore`` + * ``blink`` + * ``reverse`` + * ``conceal`` + +A color specification follows one of the the following patterns: + + * ``role=fg`` + * ``role=fg/bg`` + * ``role=fg,option,option`` + * ``role=fg/bg,option,option`` + +where ``role`` is the name of a valid color role, ``fg`` is the +foreground color, ``bg`` is the background color and each ``option`` +is one of the color modifying options. Multiple color specifications +are then separated by semicolon. For example:: + + export DJANGO_COLORS="error=yellow/blue,blink;notice=magenta" + +would specify that errors be displayed using blinking yellow on blue, +and notices displayed using magenta. All other color roles would be +left uncolored. + +Colors can also be specified by extending a base palette. If you put +a palette name in a color specification, all the colors implied by that +palette will be loaded. So:: + + export DJANGO_COLORS="light;error=yellow/blue,blink;notice=magenta" + +would specify the use of all the colors in the light color palette, +*except* for the colors for errors and notices which would be +overridden as specified. Bash completion --------------- diff --git a/docs/releases/1.2.txt b/docs/releases/1.2.txt index 0b2584dae13de..8810963eaa652 100644 --- a/docs/releases/1.2.txt +++ b/docs/releases/1.2.txt @@ -472,8 +472,8 @@ Fast Failure for Tests ---------------------- The ``test`` subcommand of ``django-admin.py``, and the ``runtests.py`` script -used to run Django's own test suite, support a new ``--failfast`` option. -When specified, this option causes the test runner to exit after +used to run Django's own test suite, support a new ``--failfast`` option. +When specified, this option causes the test runner to exit after encountering a failure instead of continuing with the test run. Improved localization @@ -492,3 +492,10 @@ Added ``readonly_fields`` to ``ModelAdmin`` :attr:`django.contrib.admin.ModelAdmin.readonly_fields` has been added to enable non-editable fields in add/change pages for models and inlines. Field and calculated values can be displayed along side editable fields. + +Customizable syntax highlighting +-------------------------------- + +You can now use the ``DJANGO_COLORS`` environment variable to modify +or disable the colors used by ``django-admin.py`` to provide +:ref:`syntax highlighting `. diff --git a/tests/regressiontests/utils/termcolors.py b/tests/regressiontests/utils/termcolors.py new file mode 100644 index 0000000000000..7c8fe0d63bbfb --- /dev/null +++ b/tests/regressiontests/utils/termcolors.py @@ -0,0 +1,149 @@ +from unittest import TestCase + +from django.utils.termcolors import parse_color_setting, PALETTES, DEFAULT_PALETTE, LIGHT_PALETTE, DARK_PALETTE, NOCOLOR_PALETTE + +class TermColorTests(TestCase): + + def test_empty_string(self): + self.assertEquals(parse_color_setting(''), PALETTES[DEFAULT_PALETTE]) + + def test_simple_palette(self): + self.assertEquals(parse_color_setting('light'), PALETTES[LIGHT_PALETTE]) + self.assertEquals(parse_color_setting('dark'), PALETTES[DARK_PALETTE]) + self.assertEquals(parse_color_setting('nocolor'), None) + + def test_fg(self): + self.assertEquals(parse_color_setting('error=green'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + + def test_fg_bg(self): + self.assertEquals(parse_color_setting('error=green/blue'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'bg':'blue'})) + + def test_fg_opts(self): + self.assertEquals(parse_color_setting('error=green,blink'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'opts': ('blink',)})) + self.assertEquals(parse_color_setting('error=green,bold,blink'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'opts': ('blink','bold')})) + + def test_fg_bg_opts(self): + self.assertEquals(parse_color_setting('error=green/blue,blink'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'bg':'blue', 'opts': ('blink',)})) + self.assertEquals(parse_color_setting('error=green/blue,bold,blink'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'bg':'blue', 'opts': ('blink','bold')})) + + def test_override_palette(self): + self.assertEquals(parse_color_setting('light;error=green'), + dict(PALETTES[LIGHT_PALETTE], + ERROR={'fg':'green'})) + + def test_override_nocolor(self): + self.assertEquals(parse_color_setting('nocolor;error=green'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg': 'green'})) + + def test_reverse_override(self): + self.assertEquals(parse_color_setting('error=green;light'), PALETTES[LIGHT_PALETTE]) + + def test_multiple_roles(self): + self.assertEquals(parse_color_setting('error=green;sql_field=blue'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'}, + SQL_FIELD={'fg':'blue'})) + + def test_override_with_multiple_roles(self): + self.assertEquals(parse_color_setting('light;error=green;sql_field=blue'), + dict(PALETTES[LIGHT_PALETTE], + ERROR={'fg':'green'}, + SQL_FIELD={'fg':'blue'})) + + def test_empty_definition(self): + self.assertEquals(parse_color_setting(';'), None) + self.assertEquals(parse_color_setting('light;'), PALETTES[LIGHT_PALETTE]) + self.assertEquals(parse_color_setting(';;;'), None) + + def test_empty_options(self): + self.assertEquals(parse_color_setting('error=green,'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + self.assertEquals(parse_color_setting('error=green,,,'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + self.assertEquals(parse_color_setting('error=green,,blink,,'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'opts': ('blink',)})) + + def test_bad_palette(self): + self.assertEquals(parse_color_setting('unknown'), None) + + def test_bad_role(self): + self.assertEquals(parse_color_setting('unknown='), None) + self.assertEquals(parse_color_setting('unknown=green'), None) + self.assertEquals(parse_color_setting('unknown=green;sql_field=blue'), + dict(PALETTES[NOCOLOR_PALETTE], + SQL_FIELD={'fg':'blue'})) + + def test_bad_color(self): + self.assertEquals(parse_color_setting('error='), None) + self.assertEquals(parse_color_setting('error=;sql_field=blue'), + dict(PALETTES[NOCOLOR_PALETTE], + SQL_FIELD={'fg':'blue'})) + self.assertEquals(parse_color_setting('error=unknown'), None) + self.assertEquals(parse_color_setting('error=unknown;sql_field=blue'), + dict(PALETTES[NOCOLOR_PALETTE], + SQL_FIELD={'fg':'blue'})) + self.assertEquals(parse_color_setting('error=green/unknown'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + self.assertEquals(parse_color_setting('error=green/blue/something'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'bg': 'blue'})) + self.assertEquals(parse_color_setting('error=green/blue/something,blink'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'bg': 'blue', 'opts': ('blink',)})) + + def test_bad_option(self): + self.assertEquals(parse_color_setting('error=green,unknown'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + self.assertEquals(parse_color_setting('error=green,unknown,blink'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'opts': ('blink',)})) + + def test_role_case(self): + self.assertEquals(parse_color_setting('ERROR=green'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + self.assertEquals(parse_color_setting('eRrOr=green'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + + def test_color_case(self): + self.assertEquals(parse_color_setting('error=GREEN'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + self.assertEquals(parse_color_setting('error=GREEN/BLUE'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'bg':'blue'})) + + self.assertEquals(parse_color_setting('error=gReEn'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green'})) + self.assertEquals(parse_color_setting('error=gReEn/bLuE'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'bg':'blue'})) + + def test_opts_case(self): + self.assertEquals(parse_color_setting('error=green,BLINK'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'opts': ('blink',)})) + + self.assertEquals(parse_color_setting('error=green,bLiNk'), + dict(PALETTES[NOCOLOR_PALETTE], + ERROR={'fg':'green', 'opts': ('blink',)})) diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py index 6258b812003ab..fe5f463110801 100644 --- a/tests/regressiontests/utils/tests.py +++ b/tests/regressiontests/utils/tests.py @@ -9,8 +9,8 @@ import timesince import datastructures -import dateformat import itercompat + from decorators import DecoratorFromMiddlewareTests # We need this because "datastructures" uses sorted() and the tests are run in @@ -28,6 +28,7 @@ } from dateformat import * +from termcolors import * class TestUtilsHtml(TestCase):