Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/rich/cells.py: 25%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

156 statements  

1from __future__ import annotations 

2 

3from functools import lru_cache 

4from operator import itemgetter 

5from typing import Callable, NamedTuple, Sequence, Tuple 

6 

7from rich._unicode_data import load as load_cell_table 

8 

9CellSpan = Tuple[int, int, int] 

10 

11_span_get_cell_len = itemgetter(2) 

12 

13# Ranges of unicode ordinals that produce a 1-cell wide character 

14# This is non-exhaustive, but covers most common Western characters 

15_SINGLE_CELL_UNICODE_RANGES: list[tuple[int, int]] = [ 

16 (0x20, 0x7E), # Latin (excluding non-printable) 

17 (0xA0, 0xAC), 

18 (0xAE, 0x002FF), 

19 (0x00370, 0x00482), # Greek / Cyrillic 

20 (0x02500, 0x025FC), # Box drawing, box elements, geometric shapes 

21 (0x02800, 0x028FF), # Braille 

22] 

23 

24# A frozen set of characters that are a single cell wide 

25_SINGLE_CELLS = frozenset( 

26 [ 

27 character 

28 for _start, _end in _SINGLE_CELL_UNICODE_RANGES 

29 for character in map(chr, range(_start, _end + 1)) 

30 ] 

31) 

32 

33# When called with a string this will return True if all 

34# characters are single-cell, otherwise False 

35_is_single_cell_widths: Callable[[str], bool] = _SINGLE_CELLS.issuperset 

36 

37 

38class CellTable(NamedTuple): 

39 """Contains unicode data required to measure the cell widths of glyphs.""" 

40 

41 unicode_version: str 

42 widths: Sequence[tuple[int, int, int]] 

43 narrow_to_wide: frozenset[str] 

44 

45 

46@lru_cache(maxsize=4096) 

47def get_character_cell_size(character: str, unicode_version: str = "auto") -> int: 

48 """Get the cell size of a character. 

49 

50 Args: 

51 character (str): A single character. 

52 unicode_version: Unicode version, `"auto"` to auto detect, `"latest"` for the latest unicode version. 

53 

54 Returns: 

55 int: Number of cells (0, 1 or 2) occupied by that character. 

56 """ 

57 codepoint = ord(character) 

58 if codepoint and codepoint < 32 or 0x07F <= codepoint < 0x0A0: 

59 return 0 

60 table = load_cell_table(unicode_version).widths 

61 

62 last_entry = table[-1] 

63 if codepoint > last_entry[1]: 

64 return 1 

65 

66 lower_bound = 0 

67 upper_bound = len(table) - 1 

68 

69 while lower_bound <= upper_bound: 

70 index = (lower_bound + upper_bound) >> 1 

71 start, end, width = table[index] 

72 if codepoint < start: 

73 upper_bound = index - 1 

74 elif codepoint > end: 

75 lower_bound = index + 1 

76 else: 

77 return width 

78 return 1 

79 

80 

81@lru_cache(4096) 

82def cached_cell_len(text: str, unicode_version: str = "auto") -> int: 

83 """Get the number of cells required to display text. 

84 

85 This method always caches, which may use up a lot of memory. It is recommended to use 

86 `cell_len` over this method. 

87 

88 Args: 

89 text (str): Text to display. 

90 unicode_version: Unicode version, `"auto"` to auto detect, `"latest"` for the latest unicode version. 

91 

92 Returns: 

93 int: Get the number of cells required to display text. 

94 """ 

95 return _cell_len(text, unicode_version) 

96 

97 

98def cell_len(text: str, unicode_version: str = "auto") -> int: 

99 """Get the cell length of a string (length as it appears in the terminal). 

100 

101 Args: 

102 text: String to measure. 

103 unicode_version: Unicode version, `"auto"` to auto detect, `"latest"` for the latest unicode version. 

104 

105 Returns: 

106 Length of string in terminal cells. 

107 """ 

108 if len(text) < 512: 

109 return cached_cell_len(text, unicode_version) 

110 return _cell_len(text, unicode_version) 

111 

112 

113def _cell_len(text: str, unicode_version: str) -> int: 

114 """Get the cell length of a string (length as it appears in the terminal). 

115 

116 Args: 

117 text: String to measure. 

118 unicode_version: Unicode version, `"auto"` to auto detect, `"latest"` for the latest unicode version. 

119 

120 Returns: 

121 Length of string in terminal cells. 

122 """ 

123 

124 if _is_single_cell_widths(text): 

125 return len(text) 

126 

127 # "\u200d" is zero width joiner 

128 # "\ufe0f" is variation selector 16 

129 if "\u200d" not in text and "\ufe0f" not in text: 

130 # Simplest case with no unicode stuff that changes the size 

131 return sum( 

132 get_character_cell_size(character, unicode_version) for character in text 

133 ) 

134 

135 cell_table = load_cell_table(unicode_version) 

136 total_width = 0 

137 last_measured_character: str | None = None 

138 

139 SPECIAL = {"\u200d", "\ufe0f"} 

140 

141 index = 0 

142 character_count = len(text) 

143 

144 while index < character_count: 

145 character = text[index] 

146 if character in SPECIAL: 

147 if character == "\u200d": 

148 index += 1 

149 elif last_measured_character: 

150 total_width += last_measured_character in cell_table.narrow_to_wide 

151 last_measured_character = None 

152 else: 

153 if character_width := get_character_cell_size(character, unicode_version): 

154 last_measured_character = character 

155 total_width += character_width 

156 index += 1 

157 

158 return total_width 

159 

160 

161def split_graphemes( 

162 text: str, unicode_version: str = "auto" 

163) -> "tuple[list[CellSpan], int]": 

164 """Divide text into spans that define a single grapheme. 

165 

166 Args: 

167 text: String to split. 

168 unicode_version: Unicode version, `"auto"` to auto detect, `"latest"` for the latest unicode version. 

169 

170 Returns: 

171 List of spans. 

172 """ 

173 

174 cell_table = load_cell_table(unicode_version) 

175 codepoint_count = len(text) 

176 index = 0 

177 last_measured_character: str | None = None 

178 

179 total_width = 0 

180 spans: list[tuple[int, int, int]] = [] 

181 SPECIAL = {"\u200d", "\ufe0f"} 

182 while index < codepoint_count: 

183 if (character := text[index]) in SPECIAL: 

184 if character == "\u200d": 

185 # zero width joiner 

186 index += 2 

187 if spans: 

188 start, _end, cell_length = spans[-1] 

189 spans[-1] = (start, index, cell_length) 

190 elif last_measured_character: 

191 # variation selector 16 

192 index += 1 

193 if spans: 

194 start, _end, cell_length = spans[-1] 

195 if last_measured_character in cell_table.narrow_to_wide: 

196 last_measured_character = None 

197 cell_length += 1 

198 total_width += 1 

199 spans[-1] = (start, index, cell_length) 

200 continue 

201 

202 if character_width := get_character_cell_size(character, unicode_version): 

203 last_measured_character = character 

204 spans.append((index, index := index + 1, character_width)) 

205 total_width += character_width 

206 elif spans: 

207 # zero width characters are associated with the previous character 

208 start, _end, cell_length = spans[-1] 

209 spans[-1] = (start, index := index + 1, cell_length) 

210 

211 return (spans, total_width) 

212 

213 

214def _split_text( 

215 text: str, cell_position: int, unicode_version: str = "auto" 

216) -> tuple[str, str]: 

217 """Split text by cell position. 

218 

219 If the cell position falls within a double width character, it is converted to two spaces. 

220 

221 Args: 

222 text: Text to split. 

223 cell_position Offset in cells. 

224 unicode_version: Unicode version, `"auto"` to auto detect, `"latest"` for the latest unicode version. 

225 

226 Returns: 

227 Tuple to two split strings. 

228 """ 

229 if cell_position <= 0: 

230 return "", text 

231 

232 spans, cell_length = split_graphemes(text, unicode_version) 

233 

234 # Guess initial offset 

235 offset = int((cell_position / cell_length) * len(spans)) 

236 left_size = sum(map(_span_get_cell_len, spans[:offset])) 

237 

238 while True: 

239 if left_size == cell_position: 

240 if offset >= len(spans): 

241 return text, "" 

242 split_index = spans[offset][0] 

243 return text[:split_index], text[split_index:] 

244 if left_size < cell_position: 

245 start, end, cell_size = spans[offset] 

246 if left_size + cell_size > cell_position: 

247 return text[:start] + " ", " " + text[end:] 

248 offset += 1 

249 left_size += cell_size 

250 else: # left_size > cell_position 

251 start, end, cell_size = spans[offset - 1] 

252 if left_size - cell_size < cell_position: 

253 return text[:start] + " ", " " + text[end:] 

254 offset -= 1 

255 left_size -= cell_size 

256 

257 

258def split_text( 

259 text: str, cell_position: int, unicode_version: str = "auto" 

260) -> tuple[str, str]: 

261 """Split text by cell position. 

262 

263 If the cell position falls within a double width character, it is converted to two spaces. 

264 

265 Args: 

266 text: Text to split. 

267 cell_position Offset in cells. 

268 unicode_version: Unicode version, `"auto"` to auto detect, `"latest"` for the latest unicode version. 

269 

270 Returns: 

271 Tuple to two split strings. 

272 """ 

273 if _is_single_cell_widths(text): 

274 return text[:cell_position], text[cell_position:] 

275 return _split_text(text, cell_position, unicode_version) 

276 

277 

278def set_cell_size(text: str, total: int, unicode_version: str = "auto") -> str: 

279 """Adjust a string by cropping or padding with spaces such that it fits within the given number of cells. 

280 

281 Args: 

282 text: String to adjust. 

283 total: Desired size in cells. 

284 unicode_version: Unicode version. 

285 

286 Returns: 

287 A string with cell size equal to total. 

288 """ 

289 if _is_single_cell_widths(text): 

290 size = len(text) 

291 if size < total: 

292 return text + " " * (total - size) 

293 return text[:total] 

294 if total <= 0: 

295 return "" 

296 cell_size = cell_len(text) 

297 if cell_size == total: 

298 return text 

299 if cell_size < total: 

300 return text + " " * (total - cell_size) 

301 text, _ = _split_text(text, total, unicode_version) 

302 return text 

303 

304 

305def chop_cells(text: str, width: int, unicode_version: str = "auto") -> list[str]: 

306 """Split text into lines such that each line fits within the available (cell) width. 

307 

308 Args: 

309 text: The text to fold such that it fits in the given width. 

310 width: The width available (number of cells). 

311 

312 Returns: 

313 A list of strings such that each string in the list has cell width 

314 less than or equal to the available width. 

315 """ 

316 if _is_single_cell_widths(text): 

317 return [text[index : index + width] for index in range(0, len(text), width)] 

318 spans, _ = split_graphemes(text, unicode_version) 

319 line_size = 0 # Size of line in cells 

320 lines: list[str] = [] 

321 line_offset = 0 # Offset (in codepoints) of start of line 

322 for start, end, cell_size in spans: 

323 if line_size + cell_size > width: 

324 lines.append(text[line_offset:start]) 

325 line_offset = start 

326 line_size = 0 

327 line_size += cell_size 

328 if line_size: 

329 lines.append(text[line_offset:]) 

330 

331 return lines