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/__init__.py +6 -0
- flexablelib/_speedups.cp313-win_amd64.pyd +0 -0
- flexablelib/cells.py +523 -0
- flexablelib/colors.py +195 -0
- flexablelib/columns.py +905 -0
- flexablelib/fast.py +81 -0
- flexablelib/merges.py +603 -0
- flexablelib/options.py +88 -0
- flexablelib/readers.py +401 -0
- flexablelib/style_compositions.py +872 -0
- flexablelib/table.py +4211 -0
- flexablelib/table_strings.py +711 -0
- flexablelib/text_width.py +480 -0
- flexablelib/utils.py +232 -0
- flexablelib/writers.py +582 -0
- flexablelib-2.0.6.dist-info/METADATA +371 -0
- flexablelib-2.0.6.dist-info/RECORD +20 -0
- flexablelib-2.0.6.dist-info/WHEEL +5 -0
- flexablelib-2.0.6.dist-info/licenses/LICENSE +21 -0
- flexablelib-2.0.6.dist-info/top_level.txt +1 -0
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
|
+
]
|