flexablelib 2.0.6__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
flexablelib/colors.py ADDED
@@ -0,0 +1,195 @@
1
+ """
2
+ ANSI COLOUR
3
+
4
+ Colour is applied by wrapping cell text in escape sequences. Because those
5
+ sequences occupy no terminal columns, every width decision must go through
6
+ `fast.visible_width` -- see text_width.py. Getting that wrong is what produces
7
+ the misaligned coloured tables reported against prettytable (#187, #290) and
8
+ tabulate (#289, #307).
9
+
10
+ Colours may be given as:
11
+
12
+ a name 'red', 'bright_blue', 'bold', 'dim'
13
+ a 256 index 'color:93' or the int 93
14
+ a hex triple '#ff8800' true colour, 24-bit
15
+ a combination 'bold red on white'
16
+
17
+ Resolution happens once per render, not once per cell, so the parsing cost
18
+ does not scale with table size.
19
+ """
20
+
21
+ import os
22
+ import re
23
+ import sys
24
+ from typing import Optional, Union
25
+
26
+ CSI = '\x1b['
27
+ RESET = '\x1b[0m'
28
+
29
+ # Text attributes.
30
+ ATTRIBUTES = {
31
+ 'bold': 1,
32
+ 'dim': 2,
33
+ 'italic': 3,
34
+ 'underline': 4,
35
+ 'blink': 5,
36
+ 'reverse': 7,
37
+ 'hidden': 8,
38
+ 'strike': 9,
39
+ }
40
+
41
+ # The sixteen named colours. Foreground codes; background is the same plus 10.
42
+ COLOURS = {
43
+ 'black': 30,
44
+ 'red': 31,
45
+ 'green': 32,
46
+ 'yellow': 33,
47
+ 'blue': 34,
48
+ 'magenta': 35,
49
+ 'cyan': 36,
50
+ 'white': 37,
51
+ 'bright_black': 90,
52
+ 'grey': 90,
53
+ 'gray': 90,
54
+ 'bright_red': 91,
55
+ 'bright_green': 92,
56
+ 'bright_yellow': 93,
57
+ 'bright_blue': 94,
58
+ 'bright_magenta': 95,
59
+ 'bright_cyan': 96,
60
+ 'bright_white': 97,
61
+ }
62
+
63
+ _HEX_RE = re.compile(r'^#?([0-9a-fA-F]{6})$')
64
+ _INDEX_RE = re.compile(r'^(?:color|colour):(\d{1,3})$')
65
+
66
+
67
+ class ColourError(ValueError):
68
+ """Raised when a colour specification cannot be understood."""
69
+
70
+
71
+ def _colour_codes(token: str, background: bool = False) -> list:
72
+ """Translate one colour token into its SGR parameters."""
73
+ offset = 10 if background else 0
74
+
75
+ if token in COLOURS:
76
+ return [COLOURS[token] + offset]
77
+
78
+ hex_match = _HEX_RE.match(token)
79
+ if hex_match:
80
+ digits = hex_match.group(1)
81
+ red = int(digits[0:2], 16)
82
+ green = int(digits[2:4], 16)
83
+ blue = int(digits[4:6], 16)
84
+ return [48 if background else 38, 2, red, green, blue]
85
+
86
+ index_match = _INDEX_RE.match(token)
87
+ if index_match:
88
+ index = int(index_match.group(1))
89
+ if not 0 <= index <= 255:
90
+ raise ColourError(f'256-colour index out of range: {index}')
91
+ return [48 if background else 38, 5, index]
92
+
93
+ if token.isdigit():
94
+ index = int(token)
95
+ if not 0 <= index <= 255:
96
+ raise ColourError(f'256-colour index out of range: {index}')
97
+ return [48 if background else 38, 5, index]
98
+
99
+ raise ColourError(f'unknown colour: {token!r}')
100
+
101
+
102
+ def parse_spec(spec: Union[str, int, None]) -> str:
103
+ """
104
+ Turn a colour specification into the escape sequence that starts it.
105
+
106
+ Accepts the forms documented in the module docstring. Returns '' for None
107
+ or an empty spec, so callers can apply the result unconditionally.
108
+
109
+ parse_spec('bold red on white') -> '\\x1b[1;31;47m'
110
+ parse_spec('#ff8800') -> '\\x1b[38;2;255;136;0m'
111
+ """
112
+ if spec is None or spec == '':
113
+ return ''
114
+ if isinstance(spec, int):
115
+ spec = str(spec)
116
+
117
+ parameters = []
118
+ # 'on' separates foreground from background: 'bold red on white'.
119
+ foreground_part, _, background_part = spec.lower().partition(' on ')
120
+
121
+ for token in foreground_part.split():
122
+ if token in ATTRIBUTES:
123
+ parameters.append(ATTRIBUTES[token])
124
+ else:
125
+ parameters.extend(_colour_codes(token, background=False))
126
+
127
+ if background_part:
128
+ for token in background_part.split():
129
+ parameters.extend(_colour_codes(token, background=True))
130
+
131
+ if not parameters:
132
+ return ''
133
+ return CSI + ';'.join(str(parameter) for parameter in parameters) + 'm'
134
+
135
+
136
+ def colorize(text: str, spec: Union[str, int, None]) -> str:
137
+ """
138
+ Wrap text in the given colour, closing it with a reset.
139
+
140
+ Returns the text untouched when the spec is empty, so this is safe to call
141
+ on every cell whether or not colour is configured.
142
+ """
143
+ prefix = parse_spec(spec)
144
+ if not prefix:
145
+ return text
146
+ return prefix + text + RESET
147
+
148
+
149
+ def supports_color(stream=None) -> bool:
150
+ """
151
+ Whether colour should be emitted to the given stream.
152
+
153
+ Honours the NO_COLOR convention (https://no-color.org): any value, even
154
+ empty, disables colour. FORCE_COLOR overrides in the other direction, which
155
+ is what CI systems and pipes into `less -R` need.
156
+
157
+ Otherwise colour is emitted only to a terminal, so redirecting output to a
158
+ file does not fill it with escape sequences.
159
+ """
160
+ if 'NO_COLOR' in os.environ:
161
+ return False
162
+ if os.environ.get('FORCE_COLOR'):
163
+ return True
164
+
165
+ if stream is None:
166
+ stream = sys.stdout
167
+
168
+ if os.environ.get('TERM') == 'dumb':
169
+ return False
170
+
171
+ try:
172
+ return bool(stream.isatty())
173
+ except (AttributeError, ValueError):
174
+ # Detached or closed stream. Assume no colour.
175
+ return False
176
+
177
+
178
+ def strip_if_unsupported(text: str, stream=None) -> str:
179
+ """Remove colour from text when the destination cannot display it."""
180
+ if supports_color(stream):
181
+ return text
182
+ from .fast import strip_ansi
183
+ return strip_ansi(text)
184
+
185
+
186
+ __all__ = [
187
+ 'colorize',
188
+ 'parse_spec',
189
+ 'supports_color',
190
+ 'strip_if_unsupported',
191
+ 'ColourError',
192
+ 'COLOURS',
193
+ 'ATTRIBUTES',
194
+ 'RESET',
195
+ ]