├── .gitignore ├── LICENSE ├── Makefile ├── README.rst ├── color.py ├── color_compat.py ├── color_simple.py ├── conftest.py ├── dev-requirements.txt ├── screenshot.png ├── test ├── color_simple_test.py ├── color_test.py ├── combination_test.py ├── memorize_test.py ├── notty_test.py ├── rgbxterm_test.py └── showcase.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac # 2 | .DS_Store 3 | 4 | # Vim swap files # 5 | *.sw[po] 6 | 7 | # Byte-compiled 8 | *.py[cod] 9 | 10 | # Distribution / packaging 11 | /build/ 12 | /dist/ 13 | *.egg-info/ 14 | 15 | # Sphinx documentation 16 | docs/_build/ 17 | 18 | # Others # 19 | .virtualenv 20 | .tox/ 21 | htmlcov/ 22 | .coverage 23 | .python-version 24 | .mypy_cache/ 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Xiao Meng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | nosetests -vs test/ 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Python Terminal Color 2 | ===================== 3 | 4 | .. image:: screenshot.png 5 | 6 | Introduction 7 | ------------ 8 | 9 | 1. This is a drop-in library for print colorized output in terminal. 10 | 2. It has no pypi package, which means you can't install it through pip. 11 | 3. It is recommended to be used as a submodule of your own project, 12 | so that no dependency will be involved. 13 | 4. ``color.py`` is Python 3 only and recommended to choose; ``color_compat.py`` is Python 2/3 compatible, only use it if you still struggle in the Python 2 morass. 14 | 15 | 16 | Usage 17 | ----- 18 | 19 | Copy the ``color.py`` file to your project, then: 20 | 21 | .. code:: python 22 | 23 | from yourproject import color 24 | 25 | # 8 bit color 26 | print(color.red('red') + color.green('green') + color.blue('blue')) 27 | print(color.bold(color.yellow('bold yellow')) + color.underline(color.cyan('underline cyan'))) 28 | print(color.magenta_hl('magenta highlight')) 29 | 30 | # xterm 256 color 31 | print(color.bg256('A9D5DE', color.fg256('276F86', 'Info!'))) 32 | print(color.bg256('E0B4B4', color.fg256('912D2B', 'Warning!'))) 33 | print(color.hl256('10a3a3', 'Teal')) 34 | 35 | 36 | Note: 37 | 38 | 1. Every color function receives and returns string, so that the result 39 | could be used with any other strings, in any string formatting situation. 40 | 41 | 2. If you pass a str type string, the color function will return a str. 42 | If you pass a bytes type string, the color function will return a bytes. 43 | 44 | 3. Color functions could be composed together, like put ``red`` into ``bold``, 45 | or put ``bg256`` into ``fg256``. ``xxx_hl`` and ``hl256`` are mostly used 46 | independently. 47 | 48 | 49 | API 50 | --- 51 | 52 | function ``(s)`` 53 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 54 | 55 | Decorate string with specified color. 56 | 57 | ``color_function`` is one of below function names: 58 | 59 | ======== ============ =========== 60 | Colors Background Highlight 61 | ======== ============ =========== 62 | black black_bg black_hl 63 | red red_bg red_hl 64 | green green_bg green_hl 65 | yellow yellow_bg yellow_hl 66 | blue blue_bg blue_hl 67 | magenta magenta_bg magenta_hl 68 | cyan cyan_bg cyan_hl 69 | white white_bg white_hl 70 | ======== ============ =========== 71 | 72 | A color function with ``_bg`` suffix means it will set color as background. 73 | A color function with ``_hl`` suffix means it will set color as background, 74 | and change the foreground as well to make the word standout. 75 | 76 | Parameters: 77 | 78 | :param str s: The input string 79 | :return: The decorated string 80 | :rtype: string 81 | :raises ValueError: if the message_body exceeds 160 characters 82 | 83 | function ``(s)`` 84 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 85 | 86 | Decorate string with specified style. 87 | 88 | ``style_function`` is one of below function names: 89 | 90 | - bold 91 | - italic 92 | - underline 93 | - strike 94 | - blink 95 | 96 | Arguments and return are the same as ``color_function``. 97 | 98 | 99 | function ``<256_color_function>(hexrgb, s)`` 100 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 101 | 102 | Decorate string with specified hex rgb color 103 | 104 | ``256_color_function`` is one of below function names: 105 | 106 | - fg256: will set color as foreground. 107 | - bg256: will set color as background. 108 | - hl256: will highlight input with the color. 109 | 110 | Parameters: 111 | 112 | :param str hexrgb: The hex rgb color string, accept length 3 and 6. eg: ``555``, ``912D2B`` 113 | :param str s: The input string 114 | :return: The decorated string 115 | :rtype: string 116 | :raises ValueError: If the input string's length not equal to 3 or 6. 117 | -------------------------------------------------------------------------------- /color.py: -------------------------------------------------------------------------------- 1 | """ 2 | color.py 3 | ======== 4 | 5 | Usage 6 | ----- 7 | 8 | >>> import color 9 | >>> 10 | >>> # 8-bit color 11 | >>> print(red('red') + green('green') + blue('blue')) 12 | >>> print(bold(yellow('bold yellow')) + underline(cyan('underline cyan'))) 13 | >>> print(magenta_hl('magenta highlight')) 14 | >>> 15 | >>> # xterm 256 color 16 | >>> print(bg256('A9D5DE', fg256('276F86', 'Info!'))) 17 | >>> print(bg256('E0B4B4', fg256('912D2B', 'Warning!'))) 18 | >>> print(hl256('10a3a3', 'Teal')) 19 | 20 | Note: 21 | 22 | 1. Every color function receives and returns string, so that the result 23 | could be used with any other strings, in any string formatting situation. 24 | 25 | 2. If you pass a str type string, the color function will return a str. 26 | If you pass a bytes type string, the color function will return a bytes string. 27 | 28 | 3. Color functions could be composed together, like put ``red`` into ``bold``, 29 | or put ``bg256`` into ``fg256``. ``xxx_hl`` and ``hl256`` are mostly used 30 | independently. 31 | 32 | API 33 | --- 34 | 35 | 8-bit colors: 36 | 37 | ======== ============ =========== 38 | Colors Background Highlight 39 | ======== ============ =========== 40 | black black_bg black_hl 41 | red red_bg red_hl 42 | green green_bg green_hl 43 | yellow yellow_bg yellow_hl 44 | blue blue_bg blue_hl 45 | magenta magenta_bg magenta_hl 46 | cyan cyan_bg cyan_hl 47 | white white_bg white_hl 48 | ======== ============ =========== 49 | 50 | Styles: 51 | - bold 52 | - italic 53 | - underline 54 | - strike 55 | - blink 56 | 57 | .. py:function:: (s) 58 | 59 | Decorate string with specified color or style. 60 | 61 | A color function with ``_bg`` suffix means it will set color as background. 62 | A color function with ``_hl`` suffix means it will set color as background, 63 | and change the foreground as well to make the word standout. 64 | 65 | :param str s: The input string 66 | :return: The decorated string 67 | :rtype: string 68 | :raises ValueError: if the message_body exceeds 160 characters 69 | 70 | 71 | 256 colors: 72 | - fg256 73 | - bg256 74 | - hl256 75 | 76 | .. py:function:: <256_color_function>(hexrgb, s) 77 | 78 | Decorate string with specified hex rgb color 79 | 80 | ``fg256`` will set color as foreground. 81 | ``bg256`` will set color as background. 82 | ``hg256`` will highlight input with the color. 83 | 84 | :param str hexrgb: The hex rgb color string, accept length 3 and 6. eg: ``555``, ``912D2B`` 85 | :param str s: The input string 86 | :return: The decorated string 87 | :rtype: string 88 | :raises ValueError: If the input string's length not equal to 3 or 6. 89 | """ 90 | 91 | from typing import Union, Any, Callable, Optional, Tuple, List, Dict 92 | import sys 93 | 94 | 95 | _use_color_no_tty = True 96 | 97 | 98 | def use_color_no_tty(flag): 99 | global _use_color_no_tty 100 | _use_color_no_tty = flag 101 | 102 | 103 | def use_color(): 104 | if sys.stdout.isatty(): 105 | return True 106 | if _use_color_no_tty: 107 | return True 108 | return False 109 | 110 | 111 | def esc(*codes: Union[int, str]) -> str: 112 | """Produces an ANSI escape code from a list of integers 113 | :rtype: text_type 114 | """ 115 | return '\x1b[{}m'.format(';'.join(str(c) for c in codes)) 116 | 117 | 118 | ############################################################################### 119 | # 8 bit Color 120 | ############################################################################### 121 | 122 | def make_color(start, end: str) -> Callable[[str], str]: 123 | def color_func(s: str) -> str: 124 | if not use_color(): 125 | return s 126 | 127 | # render 128 | return start + s + end 129 | 130 | return color_func 131 | 132 | 133 | # According to https://en.wikipedia.org/wiki/ANSI_escape_code#graphics , 134 | # 39 is reset for foreground, 49 is reset for background, 0 is reset for all 135 | # we can use 0 for convenience, but it will make color combination behaves weird. 136 | END = esc(0) 137 | 138 | FG_END = esc(39) 139 | black = make_color(esc(30), FG_END) 140 | red = make_color(esc(31), FG_END) 141 | green = make_color(esc(32), FG_END) 142 | yellow = make_color(esc(33), FG_END) 143 | blue = make_color(esc(34), FG_END) 144 | magenta = make_color(esc(35), FG_END) 145 | cyan = make_color(esc(36), FG_END) 146 | white = make_color(esc(37), FG_END) 147 | 148 | BG_END = esc(49) 149 | black_bg = make_color(esc(40), BG_END) 150 | red_bg = make_color(esc(41), BG_END) 151 | green_bg = make_color(esc(42), BG_END) 152 | yellow_bg = make_color(esc(43), BG_END) 153 | blue_bg = make_color(esc(44), BG_END) 154 | magenta_bg = make_color(esc(45), BG_END) 155 | cyan_bg = make_color(esc(46), BG_END) 156 | white_bg = make_color(esc(47), BG_END) 157 | 158 | HL_END = esc(22, 27, 39) 159 | #HL_END = esc(22, 27, 0) 160 | 161 | black_hl = make_color(esc(1, 30, 7), HL_END) 162 | red_hl = make_color(esc(1, 31, 7), HL_END) 163 | green_hl = make_color(esc(1, 32, 7), HL_END) 164 | yellow_hl = make_color(esc(1, 33, 7), HL_END) 165 | blue_hl = make_color(esc(1, 34, 7), HL_END) 166 | magenta_hl = make_color(esc(1, 35, 7), HL_END) 167 | cyan_hl = make_color(esc(1, 36, 7), HL_END) 168 | white_hl = make_color(esc(1, 37, 7), HL_END) 169 | 170 | bold = make_color(esc(1), esc(22)) 171 | italic = make_color(esc(3), esc(23)) 172 | underline = make_color(esc(4), esc(24)) 173 | strike = make_color(esc(9), esc(29)) 174 | blink = make_color(esc(5), esc(25)) 175 | 176 | 177 | ############################################################################### 178 | # Xterm 256 Color (delete if you don't need) 179 | ############################################################################### 180 | # 181 | # Rewrite from: https://gist.github.com/MicahElliott/719710 182 | 183 | import re # NOQA 184 | 185 | # Default color levels for the color cube 186 | CUBELEVELS: List[int] = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] 187 | 188 | # Generate a list of midpoints of the above list 189 | SNAPS: List[int] = [(x + y) // 2 for x, y in list(zip(CUBELEVELS, [0] + CUBELEVELS))[1:]] 190 | 191 | # Gray-scale range. 192 | _GRAYSCALE = [ 193 | (0x08, 232), # 0x08 means 080808 in HEX color 194 | (0x12, 233), 195 | (0x1c, 234), 196 | (0x26, 235), 197 | (0x30, 236), 198 | (0x3a, 237), 199 | (0x44, 238), 200 | (0x4e, 239), 201 | (0x58, 240), 202 | (0x62, 241), 203 | (0x6c, 242), 204 | (0x76, 243), 205 | (0x80, 244), 206 | (0x8a, 245), 207 | (0x94, 246), 208 | (0x9e, 247), 209 | (0xa8, 248), 210 | (0xb2, 249), 211 | (0xbc, 250), 212 | (0xc6, 251), 213 | (0xd0, 252), 214 | (0xda, 253), 215 | (0xe4, 254), 216 | (0xee, 255), 217 | ] 218 | GRAYSCALE: Dict[int, int] = dict(_GRAYSCALE) 219 | 220 | GRAYSCALE_POINTS: List[int] = [i for i, _ in _GRAYSCALE] 221 | 222 | 223 | def get_closest(v: int, l: list): 224 | return min(l, key=lambda x: abs(x - v)) 225 | 226 | 227 | class Memorize(dict): 228 | def __init__(self, func): 229 | self.func = func 230 | self.__doc__ = func.__doc__ 231 | 232 | def __call__(self, *args): 233 | return self[args] 234 | 235 | def __missing__(self, key): 236 | result = self[key] = self.func(*key) 237 | return result 238 | 239 | 240 | def memorize(func) -> Callable: 241 | func._cache = {} 242 | 243 | def wrapper(*args, **kwargs): 244 | if kwargs: 245 | return func(*args, **kwargs) 246 | if args not in func._cache: 247 | func._cache[args] = func(*args, **kwargs) 248 | return func._cache[args] 249 | 250 | for i in ('__module__', '__name__', '__doc__'): 251 | setattr(wrapper, i, getattr(func, i)) 252 | wrapper.__dict__.update(getattr(func, '__dict__', {})) # type: ignore 253 | wrapper._origin = func # type: ignore 254 | return wrapper 255 | 256 | 257 | @memorize 258 | def rgb_to_xterm(r: int, g: int, b: int) -> int: 259 | """ Converts RGB values to the nearest equivalent xterm-256 color. 260 | """ 261 | if r == g == b: 262 | # use gray scale 263 | gs = get_closest(r, GRAYSCALE_POINTS) 264 | return GRAYSCALE[gs] 265 | # Using list of snap points, convert RGB value to cube indexes 266 | r, g, b = map(lambda x: len(tuple(s for s in SNAPS if s < x)), (r, g, b)) 267 | # Simple colorcube transform 268 | return r * 36 + g * 6 + b + 16 269 | 270 | 271 | @memorize 272 | def hex_to_rgb(hx: str) -> Tuple[int, int, int]: 273 | hxlen = len(hx) 274 | if hxlen != 3 and hxlen != 6: 275 | raise ValueError('hx color must be of length 3 or 6') 276 | if hxlen == 3: 277 | hx = t_('').join(i * 2 for i in hx) 278 | parts = [int(h, 16) for h in re.split(t_(r'(..)(..)(..)'), hx)[1:4]] 279 | return tuple(parts) # type: ignore 280 | 281 | 282 | def make_256(start: str, end: str) -> Callable[[Union[tuple, str], str, Optional[Tuple[int, int, int]]], str]: 283 | def rgb_func(rgb: Union[tuple, str], s: str, x: Optional[Tuple[int, int, int]] = None) -> str: 284 | """ 285 | :param rgb: (R, G, B) tuple, or RRGGBB hex string 286 | """ 287 | if not use_color(): 288 | return s 289 | 290 | t = t_(s) 291 | 292 | # render 293 | if not isinstance(rgb, tuple): 294 | rgb = hex_to_rgb(t_(rgb)) 295 | if x is not None: 296 | xcolor = x 297 | else: 298 | xcolor = rgb_to_xterm(*rgb) 299 | 300 | tpl = start + t_('{s}') + end 301 | f = tpl.format( 302 | x=xcolor, 303 | s=t) 304 | 305 | return f 306 | 307 | return rgb_func 308 | 309 | 310 | fg256 = make_256(esc(38, 5, t_('{x}')), esc(39)) 311 | bg256 = make_256(esc(48, 5, t_('{x}')), esc(49)) 312 | hl256 = make_256(esc(1, 38, 5, t_('{x}'), 7), esc(27, 39, 22)) 313 | 314 | _grayscale_xterm_codes = [i for _, i in _GRAYSCALE] 315 | grayscale = {(i - _grayscale_xterm_codes[0]): make_color(esc(38, 5, i), esc(39)) for i in _grayscale_xterm_codes} 316 | grayscale_bg = {(i - _grayscale_xterm_codes[0]): make_color(esc(48, 5, i), esc(49)) for i in _grayscale_xterm_codes} 317 | grayscale_hl = {(i - _grayscale_xterm_codes[0]): make_color(esc(1, 38, 5, i, 7), esc(27, 39, 22)) for i in _grayscale_xterm_codes} 318 | -------------------------------------------------------------------------------- /color_compat.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | color.py 4 | ======== 5 | 6 | Usage 7 | ----- 8 | 9 | >>> import color 10 | >>> 11 | >>> # 8-bit color 12 | >>> print(red('red') + green('green') + blue('blue')) 13 | >>> print(bold(yellow('bold yellow')) + underline(cyan('underline cyan'))) 14 | >>> print(magenta_hl('magenta highlight')) 15 | >>> 16 | >>> # xterm 256 color 17 | >>> print(bg256('A9D5DE', fg256('276F86', 'Info!'))) 18 | >>> print(bg256('E0B4B4', fg256('912D2B', 'Warning!'))) 19 | >>> print(hl256('10a3a3', 'Teal')) 20 | 21 | Note: 22 | 23 | 1. Every color function receives and returns string/unicode, so that the result 24 | could be used with any other strings, in any string formatting situation. 25 | 26 | 2. If you pass a str type string, the color function will return a str. 27 | If you pass a unicode type string, the color function will return a unicode. 28 | 29 | 3. Color functions could be composed together, like put ``red`` into ``bold``, 30 | or put ``bg256`` into ``fg256``. ``xxx_hl`` and ``hl256`` are mostly used 31 | independently. 32 | 33 | API 34 | --- 35 | 36 | 8-bit colors: 37 | 38 | ======== ============ =========== 39 | Colors Background Highlight 40 | ======== ============ =========== 41 | black black_bg black_hl 42 | red red_bg red_hl 43 | green green_bg green_hl 44 | yellow yellow_bg yellow_hl 45 | blue blue_bg blue_hl 46 | magenta magenta_bg magenta_hl 47 | cyan cyan_bg cyan_hl 48 | white white_bg white_hl 49 | ======== ============ =========== 50 | 51 | Styles: 52 | - bold 53 | - italic 54 | - underline 55 | - strike 56 | - blink 57 | 58 | .. py:function:: (s) 59 | 60 | Decorate string with specified color or style. 61 | 62 | A color function with ``_bg`` suffix means it will set color as background. 63 | A color function with ``_hl`` suffix means it will set color as background, 64 | and change the foreground as well to make the word standout. 65 | 66 | :param str s: The input string (or unicode) 67 | :return: The decorated string (or unicode) 68 | :rtype: string, unicode 69 | :raises ValueError: if the message_body exceeds 160 characters 70 | 71 | 72 | 256 colors: 73 | - fg256 74 | - bg256 75 | - hl256 76 | 77 | .. py:function:: <256_color_function>(hexrgb, s) 78 | 79 | Decorate string with specified hex rgb color 80 | 81 | ``fg256`` will set color as foreground. 82 | ``bg256`` will set color as background. 83 | ``hg256`` will highlight input with the color. 84 | 85 | :param str hexrgb: The hex rgb color string, accept length 3 and 6. eg: ``555``, ``912D2B`` 86 | :param str s: The input string (or unicode) 87 | :return: The decorated string (or unicode) 88 | :rtype: string, unicode 89 | :raises ValueError: If the input string's length not equal to 3 or 6. 90 | """ 91 | 92 | import sys 93 | 94 | 95 | PY2 = sys.version_info.major == 2 96 | if not PY2: 97 | unicode = str 98 | 99 | _use_color_no_tty = True 100 | 101 | 102 | def use_color_no_tty(flag): 103 | global _use_color_no_tty 104 | _use_color_no_tty = flag 105 | 106 | 107 | def use_color(): 108 | if sys.stdout.isatty(): 109 | return True 110 | if _use_color_no_tty: 111 | return True 112 | return False 113 | 114 | 115 | def esc(*codes): 116 | # type (...) -> Text 117 | """Produces an ANSI escape code unicode from a list of integers 118 | :rtype: text_type 119 | """ 120 | return t_('\x1b[{}m').format(t_(';').join(t_(str(c)) for c in codes)) 121 | 122 | 123 | def t_(b): 124 | # type: (Union[bytes, Any]) -> Text 125 | """ensure text type""" 126 | if PY2: 127 | if isinstance(b, str): 128 | return b.decode('utf8') 129 | return b 130 | if isinstance(b, bytes): 131 | return b.decode() 132 | return b 133 | 134 | 135 | def b_(t): 136 | # type: (Union[Text, Any]) -> bytes 137 | """ensure binary type""" 138 | if PY2: 139 | if isinstance(t, unicode): 140 | return t.encode('utf8') 141 | return t 142 | if isinstance(t, str): 143 | return t.encode() 144 | return t 145 | 146 | 147 | ############################################################################### 148 | # 8 bit Color 149 | ############################################################################### 150 | 151 | def make_color(start, end): 152 | # type: (Text, Text) -> Callable 153 | def color_func(s): 154 | # type: (AnyStr) -> Text 155 | if not use_color(): 156 | return s 157 | 158 | # render 159 | return start + t_(s) + end 160 | 161 | return color_func 162 | 163 | 164 | # According to https://en.wikipedia.org/wiki/ANSI_escape_code#graphics , 165 | # 39 is reset for foreground, 49 is reset for background, 0 is reset for all 166 | # we can use 0 for convenience, but it will make color combination behaves weird. 167 | END = esc(0) 168 | 169 | FG_END = esc(39) 170 | black = make_color(esc(30), FG_END) 171 | red = make_color(esc(31), FG_END) 172 | green = make_color(esc(32), FG_END) 173 | yellow = make_color(esc(33), FG_END) 174 | blue = make_color(esc(34), FG_END) 175 | magenta = make_color(esc(35), FG_END) 176 | cyan = make_color(esc(36), FG_END) 177 | white = make_color(esc(37), FG_END) 178 | 179 | BG_END = esc(49) 180 | black_bg = make_color(esc(40), BG_END) 181 | red_bg = make_color(esc(41), BG_END) 182 | green_bg = make_color(esc(42), BG_END) 183 | yellow_bg = make_color(esc(43), BG_END) 184 | blue_bg = make_color(esc(44), BG_END) 185 | magenta_bg = make_color(esc(45), BG_END) 186 | cyan_bg = make_color(esc(46), BG_END) 187 | white_bg = make_color(esc(47), BG_END) 188 | 189 | HL_END = esc(22, 27, 39) 190 | #HL_END = esc(22, 27, 0) 191 | 192 | black_hl = make_color(esc(1, 30, 7), HL_END) 193 | red_hl = make_color(esc(1, 31, 7), HL_END) 194 | green_hl = make_color(esc(1, 32, 7), HL_END) 195 | yellow_hl = make_color(esc(1, 33, 7), HL_END) 196 | blue_hl = make_color(esc(1, 34, 7), HL_END) 197 | magenta_hl = make_color(esc(1, 35, 7), HL_END) 198 | cyan_hl = make_color(esc(1, 36, 7), HL_END) 199 | white_hl = make_color(esc(1, 37, 7), HL_END) 200 | 201 | bold = make_color(esc(1), esc(22)) 202 | italic = make_color(esc(3), esc(23)) 203 | underline = make_color(esc(4), esc(24)) 204 | strike = make_color(esc(9), esc(29)) 205 | blink = make_color(esc(5), esc(25)) 206 | 207 | 208 | ############################################################################### 209 | # Xterm 256 Color (delete if you don't need) 210 | ############################################################################### 211 | # 212 | # Rewrite from: https://gist.github.com/MicahElliott/719710 213 | 214 | import re # NOQA 215 | 216 | # Default color levels for the color cube 217 | CUBELEVELS = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] 218 | 219 | # Generate a list of midpoints of the above list 220 | SNAPS = [(x + y) / 2 for x, y in list(zip(CUBELEVELS, [0] + CUBELEVELS))[1:]] 221 | 222 | # Gray-scale range. 223 | _GRAYSCALE = [ 224 | (0x08, 232), # 0x08 means 080808 in HEX color 225 | (0x12, 233), 226 | (0x1c, 234), 227 | (0x26, 235), 228 | (0x30, 236), 229 | (0x3a, 237), 230 | (0x44, 238), 231 | (0x4e, 239), 232 | (0x58, 240), 233 | (0x62, 241), 234 | (0x6c, 242), 235 | (0x76, 243), 236 | (0x80, 244), 237 | (0x8a, 245), 238 | (0x94, 246), 239 | (0x9e, 247), 240 | (0xa8, 248), 241 | (0xb2, 249), 242 | (0xbc, 250), 243 | (0xc6, 251), 244 | (0xd0, 252), 245 | (0xda, 253), 246 | (0xe4, 254), 247 | (0xee, 255), 248 | ] 249 | GRAYSCALE = dict(_GRAYSCALE) 250 | 251 | GRAYSCALE_POINTS = [i for i, _ in _GRAYSCALE] 252 | 253 | 254 | def get_closest(v, l): 255 | return min(l, key=lambda x: abs(x - v)) 256 | 257 | 258 | class Memorize(dict): 259 | def __init__(self, func): 260 | self.func = func 261 | self.__doc__ = func.__doc__ 262 | 263 | def __call__(self, *args): 264 | return self[args] 265 | 266 | def __missing__(self, key): 267 | result = self[key] = self.func(*key) 268 | return result 269 | 270 | 271 | def memorize(func): 272 | func._cache = {} 273 | 274 | def wrapper(*args, **kwargs): 275 | if kwargs: 276 | return func(*args, **kwargs) 277 | if args not in func._cache: 278 | func._cache[args] = func(*args, **kwargs) 279 | return func._cache[args] 280 | 281 | for i in ('__module__', '__name__', '__doc__'): 282 | setattr(wrapper, i, getattr(func, i)) 283 | wrapper.__dict__.update(getattr(func, '__dict__', {})) 284 | wrapper._origin = func 285 | return wrapper 286 | 287 | 288 | @memorize 289 | def rgb_to_xterm(r, g, b): 290 | # type: (int, int, int) -> int 291 | """ Converts RGB values to the nearest equivalent xterm-256 color. 292 | """ 293 | if r == g == b: 294 | # use gray scale 295 | gs = get_closest(r, GRAYSCALE_POINTS) 296 | return GRAYSCALE[gs] 297 | # Using list of snap points, convert RGB value to cube indexes 298 | r, g, b = map(lambda x: len(tuple(s for s in SNAPS if s < x)), (r, g, b)) 299 | # Simple colorcube transform 300 | return r * 36 + g * 6 + b + 16 301 | 302 | 303 | @memorize 304 | def hex_to_rgb(hx): 305 | # type: (Text) -> Tuple[int, int, int] 306 | hxlen = len(hx) 307 | if hxlen != 3 and hxlen != 6: 308 | raise ValueError('hx color must be of length 3 or 6') 309 | if hxlen == 3: 310 | hx = t_('').join(i * 2 for i in hx) 311 | parts = [int(h, 16) for h in re.split(t_(r'(..)(..)(..)'), hx)[1:4]] 312 | return tuple(parts) 313 | 314 | 315 | def make_256(start, end): 316 | # type: (Text, Text) -> Callable 317 | def rgb_func(rgb, s, x=None): 318 | # type: (Union[tuple, AnyText], AnyStr, Union[None, Tuple[int, int, int]]) -> Text 319 | """ 320 | :param rgb: (R, G, B) tuple, or RRGGBB hex string 321 | """ 322 | if not use_color(): 323 | return s 324 | 325 | t = t_(s) 326 | 327 | # render 328 | if not isinstance(rgb, tuple): 329 | rgb = hex_to_rgb(t_(rgb)) 330 | if x is not None: 331 | xcolor = x 332 | else: 333 | xcolor = rgb_to_xterm(*rgb) 334 | 335 | tpl = start + t_('{s}') + end 336 | f = tpl.format( 337 | x=xcolor, 338 | s=t) 339 | 340 | return f 341 | 342 | return rgb_func 343 | 344 | 345 | fg256 = make_256(esc(38, 5, t_('{x}')), esc(39)) 346 | bg256 = make_256(esc(48, 5, t_('{x}')), esc(49)) 347 | hl256 = make_256(esc(1, 38, 5, t_('{x}'), 7), esc(27, 39, 22)) 348 | 349 | _grayscale_xterm_codes = [i for _, i in _GRAYSCALE] 350 | grayscale = {(i - _grayscale_xterm_codes[0]): make_color(esc(38, 5, i), esc(39)) for i in _grayscale_xterm_codes} 351 | grayscale_bg = {(i - _grayscale_xterm_codes[0]): make_color(esc(48, 5, i), esc(49)) for i in _grayscale_xterm_codes} 352 | grayscale_hl = {(i - _grayscale_xterm_codes[0]): make_color(esc(1, 38, 5, i, 7), esc(27, 39, 22)) for i in _grayscale_xterm_codes} 353 | -------------------------------------------------------------------------------- /color_simple.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | 4 | def make_color(code): 5 | def color_func(s): 6 | tpl = '\x1b[{}m{}\x1b[0m' 7 | return tpl.format(code, s) 8 | return color_func 9 | 10 | red = make_color(31) 11 | green = make_color(32) 12 | yellow = make_color(33) 13 | blue = make_color(34) 14 | magenta = make_color(35) 15 | cyan = make_color(36) 16 | 17 | bold = make_color(1) 18 | underline = make_color(4) 19 | 20 | grayscale = {(i - 232): make_color('38;5;' + str(i)) for i in range(232, 256)} 21 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | if os.getenv('COLOR_COMPAT'): 6 | print('use color_compat.py') 7 | import color_compat 8 | sys.modules['color'] = sys.modules['color_compat'] 9 | else: 10 | print('use color.py') 11 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | mypy 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reorx/python-terminal-color/b67bdc4e2547042b1f5eef9f24b7a30e60c62101/screenshot.png -------------------------------------------------------------------------------- /test/color_simple_test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import color_simple 4 | 5 | color_names = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan'] 6 | 7 | compare = color_simple.red('compare') + 'white' 8 | 9 | 10 | def test_colors(): 11 | for i in color_names: 12 | print(getattr(color_simple, i)(i) + compare) 13 | 14 | 15 | def test_styles(): 16 | for i in ('bold', 'underline'): 17 | print(getattr(color_simple, i)(i) + compare) 18 | 19 | 20 | def test_grayscale(): 21 | print() 22 | for i in sorted(color_simple.grayscale.keys()): 23 | print(color_simple.grayscale[i]('grayscale {}'.format(i)) + compare) 24 | -------------------------------------------------------------------------------- /test/color_test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import color 4 | 5 | color_names = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan'] 6 | 7 | 8 | def test_named_colors(): 9 | for i in color_names: 10 | print(getattr(color, i)(i) + color.red('compare')) 11 | print(color.black(getattr(color, i + '_bg')(i + '_bg')) + color.red('compare')) 12 | print(getattr(color, i + '_hl')(i + '_hl') + color.red('compare')) 13 | 14 | 15 | def test_decorations(): 16 | for i in ['bold', 'italic', 'underline', 'strike', 'blink']: 17 | print('#' + i) 18 | for j in color_names: 19 | c = getattr(color, j)(j) 20 | print(getattr(color, i)(c) + color.red('compare')) 21 | 22 | 23 | def test_256_colors(): 24 | for i in (list(range(10)) + ['a', 'b', 'c', 'd', 'e', 'f']): 25 | hex = str(i) * 3 26 | print(color.fg256(hex, hex), color.bg256(hex, hex), color.hl256(hex, hex)) 27 | 28 | 29 | def test_grayscale(): 30 | for i in sorted(color.grayscale): 31 | print(color.grayscale[i]('grayscale' + str(i))) 32 | for i in sorted(color.grayscale): 33 | print(color.grayscale_bg[i]('grayscale_bg' + str(i))) 34 | for i in sorted(color.grayscale): 35 | print(color.grayscale_hl[i]('grayscale_hl' + str(i))) 36 | -------------------------------------------------------------------------------- /test/combination_test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import color 4 | 5 | 6 | def test_combination(): 7 | print() 8 | tpl = color.green('the quick {} jump over the {} dog') 9 | f = tpl.format( 10 | color.yellow('brown fox'), 11 | color.red_bg('lazy'), 12 | ) 13 | print(repr(f)) 14 | print(f) 15 | 16 | tpl = color.green_bg('the quick {} jump over the {} dog') 17 | f = tpl.format( 18 | color.yellow('brown fox'), 19 | color.red_bg('lazy'), 20 | ) 21 | print(repr(f)) 22 | print(f) 23 | -------------------------------------------------------------------------------- /test/memorize_test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import color 4 | import time 5 | 6 | 7 | rgb_tuple = (100, 150, 200) 8 | 9 | 10 | def with_memorize_test(): 11 | print('Run 10000 times') 12 | t0 = time.time() 13 | for _ in range(10000): 14 | color.rgb_to_xterm(*rgb_tuple) 15 | tr = int((time.time() - t0) * 1000) 16 | print('Cost {} ms'.format(tr)) 17 | 18 | 19 | def without_memorize_test(): 20 | print('Run 10000 times') 21 | t0 = time.time() 22 | for _ in range(10000): 23 | color.rgb_to_xterm._origin(*rgb_tuple) 24 | tr = int((time.time() - t0) * 1000) 25 | print('Cost {} ms'.format(tr)) 26 | -------------------------------------------------------------------------------- /test/notty_test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import color 4 | 5 | 6 | def test_notty(): 7 | """ 8 | Run `nosetests -vs test/notty_test.py | less` to see red is not red 9 | """ 10 | color.use_color_no_tty(False) 11 | print(color.red('red')) 12 | -------------------------------------------------------------------------------- /test/rgbxterm_test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import print_function 4 | import re 5 | import sys 6 | from color import rgb_to_xterm, hex_to_rgb, t_ 7 | 8 | 9 | CLUT = [ # color look-up table 10 | # 8-bit, RGB hex 11 | 12 | # Primary 3-bit (8 colors). Unique representation! 13 | ('00', '000000'), 14 | ('01', '800000'), 15 | ('02', '008000'), 16 | ('03', '808000'), 17 | ('04', '000080'), 18 | ('05', '800080'), 19 | ('06', '008080'), 20 | ('07', 'c0c0c0'), 21 | 22 | # Equivalent "bright" versions of original 8 colors. 23 | ('08', '808080'), 24 | ('09', 'ff0000'), 25 | ('10', '00ff00'), 26 | ('11', 'ffff00'), 27 | ('12', '0000ff'), 28 | ('13', 'ff00ff'), 29 | ('14', '00ffff'), 30 | ('15', 'ffffff'), 31 | 32 | # Strictly ascending. 33 | ('16', '000000'), 34 | ('17', '00005f'), 35 | ('18', '000087'), 36 | ('19', '0000af'), 37 | ('20', '0000d7'), 38 | ('21', '0000ff'), 39 | ('22', '005f00'), 40 | ('23', '005f5f'), 41 | ('24', '005f87'), 42 | ('25', '005faf'), 43 | ('26', '005fd7'), 44 | ('27', '005fff'), 45 | ('28', '008700'), 46 | ('29', '00875f'), 47 | ('30', '008787'), 48 | ('31', '0087af'), 49 | ('32', '0087d7'), 50 | ('33', '0087ff'), 51 | ('34', '00af00'), 52 | ('35', '00af5f'), 53 | ('36', '00af87'), 54 | ('37', '00afaf'), 55 | ('38', '00afd7'), 56 | ('39', '00afff'), 57 | ('40', '00d700'), 58 | ('41', '00d75f'), 59 | ('42', '00d787'), 60 | ('43', '00d7af'), 61 | ('44', '00d7d7'), 62 | ('45', '00d7ff'), 63 | ('46', '00ff00'), 64 | ('47', '00ff5f'), 65 | ('48', '00ff87'), 66 | ('49', '00ffaf'), 67 | ('50', '00ffd7'), 68 | ('51', '00ffff'), 69 | ('52', '5f0000'), 70 | ('53', '5f005f'), 71 | ('54', '5f0087'), 72 | ('55', '5f00af'), 73 | ('56', '5f00d7'), 74 | ('57', '5f00ff'), 75 | ('58', '5f5f00'), 76 | ('59', '5f5f5f'), 77 | ('60', '5f5f87'), 78 | ('61', '5f5faf'), 79 | ('62', '5f5fd7'), 80 | ('63', '5f5fff'), 81 | ('64', '5f8700'), 82 | ('65', '5f875f'), 83 | ('66', '5f8787'), 84 | ('67', '5f87af'), 85 | ('68', '5f87d7'), 86 | ('69', '5f87ff'), 87 | ('70', '5faf00'), 88 | ('71', '5faf5f'), 89 | ('72', '5faf87'), 90 | ('73', '5fafaf'), 91 | ('74', '5fafd7'), 92 | ('75', '5fafff'), 93 | ('76', '5fd700'), 94 | ('77', '5fd75f'), 95 | ('78', '5fd787'), 96 | ('79', '5fd7af'), 97 | ('80', '5fd7d7'), 98 | ('81', '5fd7ff'), 99 | ('82', '5fff00'), 100 | ('83', '5fff5f'), 101 | ('84', '5fff87'), 102 | ('85', '5fffaf'), 103 | ('86', '5fffd7'), 104 | ('87', '5fffff'), 105 | ('88', '870000'), 106 | ('89', '87005f'), 107 | ('90', '870087'), 108 | ('91', '8700af'), 109 | ('92', '8700d7'), 110 | ('93', '8700ff'), 111 | ('94', '875f00'), 112 | ('95', '875f5f'), 113 | ('96', '875f87'), 114 | ('97', '875faf'), 115 | ('98', '875fd7'), 116 | ('99', '875fff'), 117 | ('100', '878700'), 118 | ('101', '87875f'), 119 | ('102', '878787'), 120 | ('103', '8787af'), 121 | ('104', '8787d7'), 122 | ('105', '8787ff'), 123 | ('106', '87af00'), 124 | ('107', '87af5f'), 125 | ('108', '87af87'), 126 | ('109', '87afaf'), 127 | ('110', '87afd7'), 128 | ('111', '87afff'), 129 | ('112', '87d700'), 130 | ('113', '87d75f'), 131 | ('114', '87d787'), 132 | ('115', '87d7af'), 133 | ('116', '87d7d7'), 134 | ('117', '87d7ff'), 135 | ('118', '87ff00'), 136 | ('119', '87ff5f'), 137 | ('120', '87ff87'), 138 | ('121', '87ffaf'), 139 | ('122', '87ffd7'), 140 | ('123', '87ffff'), 141 | ('124', 'af0000'), 142 | ('125', 'af005f'), 143 | ('126', 'af0087'), 144 | ('127', 'af00af'), 145 | ('128', 'af00d7'), 146 | ('129', 'af00ff'), 147 | ('130', 'af5f00'), 148 | ('131', 'af5f5f'), 149 | ('132', 'af5f87'), 150 | ('133', 'af5faf'), 151 | ('134', 'af5fd7'), 152 | ('135', 'af5fff'), 153 | ('136', 'af8700'), 154 | ('137', 'af875f'), 155 | ('138', 'af8787'), 156 | ('139', 'af87af'), 157 | ('140', 'af87d7'), 158 | ('141', 'af87ff'), 159 | ('142', 'afaf00'), 160 | ('143', 'afaf5f'), 161 | ('144', 'afaf87'), 162 | ('145', 'afafaf'), 163 | ('146', 'afafd7'), 164 | ('147', 'afafff'), 165 | ('148', 'afd700'), 166 | ('149', 'afd75f'), 167 | ('150', 'afd787'), 168 | ('151', 'afd7af'), 169 | ('152', 'afd7d7'), 170 | ('153', 'afd7ff'), 171 | ('154', 'afff00'), 172 | ('155', 'afff5f'), 173 | ('156', 'afff87'), 174 | ('157', 'afffaf'), 175 | ('158', 'afffd7'), 176 | ('159', 'afffff'), 177 | ('160', 'd70000'), 178 | ('161', 'd7005f'), 179 | ('162', 'd70087'), 180 | ('163', 'd700af'), 181 | ('164', 'd700d7'), 182 | ('165', 'd700ff'), 183 | ('166', 'd75f00'), 184 | ('167', 'd75f5f'), 185 | ('168', 'd75f87'), 186 | ('169', 'd75faf'), 187 | ('170', 'd75fd7'), 188 | ('171', 'd75fff'), 189 | ('172', 'd78700'), 190 | ('173', 'd7875f'), 191 | ('174', 'd78787'), 192 | ('175', 'd787af'), 193 | ('176', 'd787d7'), 194 | ('177', 'd787ff'), 195 | ('178', 'd7af00'), 196 | ('179', 'd7af5f'), 197 | ('180', 'd7af87'), 198 | ('181', 'd7afaf'), 199 | ('182', 'd7afd7'), 200 | ('183', 'd7afff'), 201 | ('184', 'd7d700'), 202 | ('185', 'd7d75f'), 203 | ('186', 'd7d787'), 204 | ('187', 'd7d7af'), 205 | ('188', 'd7d7d7'), 206 | ('189', 'd7d7ff'), 207 | ('190', 'd7ff00'), 208 | ('191', 'd7ff5f'), 209 | ('192', 'd7ff87'), 210 | ('193', 'd7ffaf'), 211 | ('194', 'd7ffd7'), 212 | ('195', 'd7ffff'), 213 | ('196', 'ff0000'), 214 | ('197', 'ff005f'), 215 | ('198', 'ff0087'), 216 | ('199', 'ff00af'), 217 | ('200', 'ff00d7'), 218 | ('201', 'ff00ff'), 219 | ('202', 'ff5f00'), 220 | ('203', 'ff5f5f'), 221 | ('204', 'ff5f87'), 222 | ('205', 'ff5faf'), 223 | ('206', 'ff5fd7'), 224 | ('207', 'ff5fff'), 225 | ('208', 'ff8700'), 226 | ('209', 'ff875f'), 227 | ('210', 'ff8787'), 228 | ('211', 'ff87af'), 229 | ('212', 'ff87d7'), 230 | ('213', 'ff87ff'), 231 | ('214', 'ffaf00'), 232 | ('215', 'ffaf5f'), 233 | ('216', 'ffaf87'), 234 | ('217', 'ffafaf'), 235 | ('218', 'ffafd7'), 236 | ('219', 'ffafff'), 237 | ('220', 'ffd700'), 238 | ('221', 'ffd75f'), 239 | ('222', 'ffd787'), 240 | ('223', 'ffd7af'), 241 | ('224', 'ffd7d7'), 242 | ('225', 'ffd7ff'), 243 | ('226', 'ffff00'), 244 | ('227', 'ffff5f'), 245 | ('228', 'ffff87'), 246 | ('229', 'ffffaf'), 247 | ('230', 'ffffd7'), 248 | ('231', 'ffffff'), 249 | 250 | # Gray-scale range. 251 | ('232', '080808'), 252 | ('233', '121212'), 253 | ('234', '1c1c1c'), 254 | ('235', '262626'), 255 | ('236', '303030'), 256 | ('237', '3a3a3a'), 257 | ('238', '444444'), 258 | ('239', '4e4e4e'), 259 | ('240', '585858'), 260 | ('241', '626262'), 261 | ('242', '6c6c6c'), 262 | ('243', '767676'), 263 | ('244', '808080'), 264 | ('245', '8a8a8a'), 265 | ('246', '949494'), 266 | ('247', '9e9e9e'), 267 | ('248', 'a8a8a8'), 268 | ('249', 'b2b2b2'), 269 | ('250', 'bcbcbc'), 270 | ('251', 'c6c6c6'), 271 | ('252', 'd0d0d0'), 272 | ('253', 'dadada'), 273 | ('254', 'e4e4e4'), 274 | ('255', 'eeeeee'), 275 | ] 276 | 277 | 278 | def test_rgb_to_xterm(): 279 | for v, hex in CLUT: 280 | v, hex = t_(v), t_(hex) 281 | r, g, b = hex_to_rgb(hex) 282 | term = rgb_to_xterm(r, g, b) 283 | print( 284 | t_('RGB {}{} -> xterm color \033[38;5;{}m{} ({}) \033[0m').format( 285 | hex, [r, g, b], term, term, v 286 | ) 287 | ) 288 | -------------------------------------------------------------------------------- /test/showcase.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import print_function 4 | import color 5 | from rgbxterm_test import CLUT 6 | 7 | 8 | color_names = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan'] 9 | 10 | 11 | def static_width(s, width, length=None): 12 | if length is None: 13 | length = len(s) 14 | return s + (' ' * (width - length)) 15 | 16 | 17 | if __name__ == '__main__': 18 | print() 19 | print('{} {} {}'.format( 20 | static_width('color(s)', 8), 21 | static_width('color_bg(s)', 12), 22 | static_width('color_hl(s)', 12), 23 | )) 24 | for i in color_names: 25 | t0 = i 26 | t1 = i + '_bg' 27 | t2 = i + '_hl' 28 | print('{} {} {}'.format( 29 | static_width(getattr(color, t0)(t0), 8, len(t0)), 30 | static_width(getattr(color, t1)(t1), 12, len(t1)), 31 | static_width(getattr(color, t2)(t2), 12, len(t2)), 32 | )) 33 | print() 34 | 35 | s = ''.join(color.grayscale_bg[i](' ') for i in color.grayscale_bg) 36 | print('grayscale_bg() {}'.format(s)) 37 | print() 38 | 39 | print(' bg256(hex, s)') 40 | clut = CLUT[16:-24] 41 | rl = 6 42 | while clut: 43 | r = clut[:rl] 44 | clut = clut[rl:] 45 | 46 | s = ''.join(color.bg256(hex, ' ') for _, hex in r) 47 | print('{} ~ {}: {}'.format(r[0][1], r[-1][1], s)) 48 | # s = ''.join(color.bg256(hex, ' ', x=x) for x, hex in r) 49 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py27 4 | py37 5 | skipsdist=True 6 | 7 | [testenv] 8 | setenv = 9 | LANG = C.UTF-8 10 | PYTHONPATH = {toxinidir} 11 | deps = 12 | -r{toxinidir}/dev-requirements.txt 13 | pytest-cov 14 | whitelist_externals = env 15 | commands = 16 | pytest -vv --cov=color --cov-config=tox.ini --cov-report=html 17 | env COLOR_COMPAT=1 pytest -vv 18 | mypy color.py 19 | 20 | [testenv:py27] 21 | deps = 22 | pytest 23 | commands = 24 | env COLOR_COMPAT=1 pytest -vv 25 | 26 | 27 | [pytest] 28 | python_files = test/*_test.py 29 | --------------------------------------------------------------------------------