Skip to content

Commit 881b04d

Browse files
committed
adding ansi colouring
1 parent bdcca42 commit 881b04d

2 files changed

Lines changed: 168 additions & 0 deletions

File tree

devtools/ansi.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from enum import IntEnum
2+
3+
_ansi_template = '\033[{}m'
4+
5+
6+
class Style(IntEnum):
7+
"""
8+
Heavily borrowed from https://github.com/pallets/click/blob/6.7/click/termui.py
9+
10+
Italic added and generally modernised improved.
11+
"""
12+
reset = 0
13+
14+
bold = 1
15+
un_bold = 22
16+
17+
dim = 2
18+
un_dim = 22
19+
20+
italic = 3
21+
un_italic = 23
22+
23+
underline = 4
24+
un_underline = 24
25+
26+
blink = 5
27+
un_blink = 25
28+
29+
reverse = 7
30+
un_reverse = 27
31+
32+
# foreground colours
33+
black = 30
34+
red = 31
35+
green = 32
36+
yellow = 33
37+
blue = 34
38+
magenta = 35
39+
cyan = 36
40+
white = 37
41+
fg_reset = 38
42+
43+
# background colours
44+
bg_black = 40
45+
bg_red = 41
46+
bg_green = 42
47+
bg_yellow = 43
48+
bg_blue = 44
49+
bg_magenta = 45
50+
bg_cyan = 46
51+
bg_white = 47
52+
bg_reset = 48
53+
54+
# this is a meta value used for the "Style" instance which is the "style" function
55+
function = -1
56+
57+
def __call__(self, text: str, *styles, reset: bool=True):
58+
"""
59+
Styles a text with ANSI styles and returns the new string.
60+
61+
By default the styling is cleared at the end of the string, this can be prevented with``reset=False``.
62+
63+
Examples::
64+
65+
print(style('Hello World!', style.green))
66+
print(style('ATTENTION!', style.bg_magenta))
67+
print(style('Some things', style.reverse, style.bold))
68+
69+
Supported color names:
70+
71+
* ``black`` (might be a gray)
72+
* ``red``
73+
* ``green``
74+
* ``yellow`` (might be an orange)
75+
* ``blue``
76+
* ``magenta``
77+
* ``cyan``
78+
* ``white`` (might be light gray)
79+
* ``reset`` (reset the color code only)
80+
81+
:param text: the string to style with ansi codes.
82+
:param *styles: zero or more styles to apply to the text, should be either style instances or strings
83+
matching style names.
84+
:param reset: by default a reset-all code is added at the end of the
85+
string which means that styles do not carry over. This
86+
can be disabled to compose styles.
87+
"""
88+
parts = []
89+
for s in styles:
90+
if not isinstance(s, self.__class__):
91+
try:
92+
s = self.styles[s]
93+
except KeyError:
94+
raise ValueError('invalid style "{}"'.format(s))
95+
parts.append(_ansi_template.format(s))
96+
parts.append(text)
97+
if reset:
98+
parts.append(_ansi_template.format(self.reset))
99+
return ''.join(parts)
100+
101+
@property
102+
def styles(self):
103+
return self.__class__.__members__
104+
105+
def __repr__(self):
106+
if self == self.function:
107+
return '<pseudo function style(text, *styles)>'
108+
else:
109+
return super().__repr__()
110+
111+
def __str__(self):
112+
if self == self.function:
113+
return repr(self)
114+
else:
115+
return super().__str__()
116+
117+
118+
style = Style(-1)
119+
120+
121+
def sprint(text, *styles, reset=True, flush=True, **print_kwargs):
122+
print(style(text, *styles, reset=reset), flush=flush, **print_kwargs)

tests/test_ansi.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import pytest
2+
3+
from devtools.ansi import sprint, style
4+
5+
6+
def test_colorize():
7+
v = style('hello', style.red)
8+
assert v == '\x1b[31mhello\x1b[0m', repr(v)
9+
10+
11+
def test_no_reset():
12+
v = style('hello', style.bold, reset=False)
13+
assert v == '\x1b[1mhello', repr(v)
14+
15+
16+
def test_style_str():
17+
v = style('hello', 'red')
18+
assert v == '\x1b[31mhello\x1b[0m', repr(v)
19+
20+
21+
def test_invalid_style_str():
22+
with pytest.raises(ValueError) as exc_info:
23+
style('x', 'mauve')
24+
assert exc_info.value.args[0] == 'invalid style "mauve"'
25+
26+
27+
def test_print(capsys):
28+
sprint('hello', style.green)
29+
stdout, stderr = capsys.readouterr()
30+
assert stdout == '\x1b[32mhello\x1b[0m\n', repr(stdout)
31+
assert stderr == ''
32+
33+
34+
def test_get_styles():
35+
assert style.styles['bold'] == 1
36+
assert style.styles['un_bold'] == 22
37+
38+
39+
def test_repr():
40+
assert repr(style) == '<pseudo function style(text, *styles)>'
41+
assert repr(style.red) == '<Style.red: 31>'
42+
43+
44+
def test_str():
45+
assert str(style) == '<pseudo function style(text, *styles)>'
46+
assert str(style.red) == 'Style.red'

0 commit comments

Comments
 (0)