]>
Commit | Line | Data |
---|---|---|
e0df8241 JR |
1 | import sys |
2 | from typing import TYPE_CHECKING, Iterable, List | |
3 | ||
4 | if sys.version_info >= (3, 8): | |
5 | from typing import Literal | |
6 | else: | |
7 | from pip._vendor.typing_extensions import Literal # pragma: no cover | |
8 | ||
9 | ||
10 | from ._loop import loop_last | |
11 | ||
12 | if TYPE_CHECKING: | |
13 | from pip._vendor.rich.console import ConsoleOptions | |
14 | ||
15 | ||
16 | class Box: | |
17 | """Defines characters to render boxes. | |
18 | ||
19 | ┌─┬┐ top | |
20 | │ ││ head | |
21 | ├─┼┤ head_row | |
22 | │ ││ mid | |
23 | ├─┼┤ row | |
24 | ├─┼┤ foot_row | |
25 | │ ││ foot | |
26 | └─┴┘ bottom | |
27 | ||
28 | Args: | |
29 | box (str): Characters making up box. | |
30 | ascii (bool, optional): True if this box uses ascii characters only. Default is False. | |
31 | """ | |
32 | ||
33 | def __init__(self, box: str, *, ascii: bool = False) -> None: | |
34 | self._box = box | |
35 | self.ascii = ascii | |
36 | line1, line2, line3, line4, line5, line6, line7, line8 = box.splitlines() | |
37 | # top | |
38 | self.top_left, self.top, self.top_divider, self.top_right = iter(line1) | |
39 | # head | |
40 | self.head_left, _, self.head_vertical, self.head_right = iter(line2) | |
41 | # head_row | |
42 | ( | |
43 | self.head_row_left, | |
44 | self.head_row_horizontal, | |
45 | self.head_row_cross, | |
46 | self.head_row_right, | |
47 | ) = iter(line3) | |
48 | ||
49 | # mid | |
50 | self.mid_left, _, self.mid_vertical, self.mid_right = iter(line4) | |
51 | # row | |
52 | self.row_left, self.row_horizontal, self.row_cross, self.row_right = iter(line5) | |
53 | # foot_row | |
54 | ( | |
55 | self.foot_row_left, | |
56 | self.foot_row_horizontal, | |
57 | self.foot_row_cross, | |
58 | self.foot_row_right, | |
59 | ) = iter(line6) | |
60 | # foot | |
61 | self.foot_left, _, self.foot_vertical, self.foot_right = iter(line7) | |
62 | # bottom | |
63 | self.bottom_left, self.bottom, self.bottom_divider, self.bottom_right = iter( | |
64 | line8 | |
65 | ) | |
66 | ||
67 | def __repr__(self) -> str: | |
68 | return "Box(...)" | |
69 | ||
70 | def __str__(self) -> str: | |
71 | return self._box | |
72 | ||
73 | def substitute(self, options: "ConsoleOptions", safe: bool = True) -> "Box": | |
74 | """Substitute this box for another if it won't render due to platform issues. | |
75 | ||
76 | Args: | |
77 | options (ConsoleOptions): Console options used in rendering. | |
78 | safe (bool, optional): Substitute this for another Box if there are known problems | |
79 | displaying on the platform (currently only relevant on Windows). Default is True. | |
80 | ||
81 | Returns: | |
82 | Box: A different Box or the same Box. | |
83 | """ | |
84 | box = self | |
85 | if options.legacy_windows and safe: | |
86 | box = LEGACY_WINDOWS_SUBSTITUTIONS.get(box, box) | |
87 | if options.ascii_only and not box.ascii: | |
88 | box = ASCII | |
89 | return box | |
90 | ||
91 | def get_plain_headed_box(self) -> "Box": | |
92 | """If this box uses special characters for the borders of the header, then | |
93 | return the equivalent box that does not. | |
94 | ||
95 | Returns: | |
96 | Box: The most similar Box that doesn't use header-specific box characters. | |
97 | If the current Box already satisfies this criterion, then it's returned. | |
98 | """ | |
99 | return PLAIN_HEADED_SUBSTITUTIONS.get(self, self) | |
100 | ||
101 | def get_top(self, widths: Iterable[int]) -> str: | |
102 | """Get the top of a simple box. | |
103 | ||
104 | Args: | |
105 | widths (List[int]): Widths of columns. | |
106 | ||
107 | Returns: | |
108 | str: A string of box characters. | |
109 | """ | |
110 | ||
111 | parts: List[str] = [] | |
112 | append = parts.append | |
113 | append(self.top_left) | |
114 | for last, width in loop_last(widths): | |
115 | append(self.top * width) | |
116 | if not last: | |
117 | append(self.top_divider) | |
118 | append(self.top_right) | |
119 | return "".join(parts) | |
120 | ||
121 | def get_row( | |
122 | self, | |
123 | widths: Iterable[int], | |
124 | level: Literal["head", "row", "foot", "mid"] = "row", | |
125 | edge: bool = True, | |
126 | ) -> str: | |
127 | """Get the top of a simple box. | |
128 | ||
129 | Args: | |
130 | width (List[int]): Widths of columns. | |
131 | ||
132 | Returns: | |
133 | str: A string of box characters. | |
134 | """ | |
135 | if level == "head": | |
136 | left = self.head_row_left | |
137 | horizontal = self.head_row_horizontal | |
138 | cross = self.head_row_cross | |
139 | right = self.head_row_right | |
140 | elif level == "row": | |
141 | left = self.row_left | |
142 | horizontal = self.row_horizontal | |
143 | cross = self.row_cross | |
144 | right = self.row_right | |
145 | elif level == "mid": | |
146 | left = self.mid_left | |
147 | horizontal = " " | |
148 | cross = self.mid_vertical | |
149 | right = self.mid_right | |
150 | elif level == "foot": | |
151 | left = self.foot_row_left | |
152 | horizontal = self.foot_row_horizontal | |
153 | cross = self.foot_row_cross | |
154 | right = self.foot_row_right | |
155 | else: | |
156 | raise ValueError("level must be 'head', 'row' or 'foot'") | |
157 | ||
158 | parts: List[str] = [] | |
159 | append = parts.append | |
160 | if edge: | |
161 | append(left) | |
162 | for last, width in loop_last(widths): | |
163 | append(horizontal * width) | |
164 | if not last: | |
165 | append(cross) | |
166 | if edge: | |
167 | append(right) | |
168 | return "".join(parts) | |
169 | ||
170 | def get_bottom(self, widths: Iterable[int]) -> str: | |
171 | """Get the bottom of a simple box. | |
172 | ||
173 | Args: | |
174 | widths (List[int]): Widths of columns. | |
175 | ||
176 | Returns: | |
177 | str: A string of box characters. | |
178 | """ | |
179 | ||
180 | parts: List[str] = [] | |
181 | append = parts.append | |
182 | append(self.bottom_left) | |
183 | for last, width in loop_last(widths): | |
184 | append(self.bottom * width) | |
185 | if not last: | |
186 | append(self.bottom_divider) | |
187 | append(self.bottom_right) | |
188 | return "".join(parts) | |
189 | ||
190 | ||
191 | ASCII: Box = Box( | |
192 | """\ | |
193 | +--+ | |
194 | | || | |
195 | |-+| | |
196 | | || | |
197 | |-+| | |
198 | |-+| | |
199 | | || | |
200 | +--+ | |
201 | """, | |
202 | ascii=True, | |
203 | ) | |
204 | ||
205 | ASCII2: Box = Box( | |
206 | """\ | |
207 | +-++ | |
208 | | || | |
209 | +-++ | |
210 | | || | |
211 | +-++ | |
212 | +-++ | |
213 | | || | |
214 | +-++ | |
215 | """, | |
216 | ascii=True, | |
217 | ) | |
218 | ||
219 | ASCII_DOUBLE_HEAD: Box = Box( | |
220 | """\ | |
221 | +-++ | |
222 | | || | |
223 | +=++ | |
224 | | || | |
225 | +-++ | |
226 | +-++ | |
227 | | || | |
228 | +-++ | |
229 | """, | |
230 | ascii=True, | |
231 | ) | |
232 | ||
233 | SQUARE: Box = Box( | |
234 | """\ | |
235 | ┌─┬┐ | |
236 | │ ││ | |
237 | ├─┼┤ | |
238 | │ ││ | |
239 | ├─┼┤ | |
240 | ├─┼┤ | |
241 | │ ││ | |
242 | └─┴┘ | |
243 | """ | |
244 | ) | |
245 | ||
246 | SQUARE_DOUBLE_HEAD: Box = Box( | |
247 | """\ | |
248 | ┌─┬┐ | |
249 | │ ││ | |
250 | ╞═╪╡ | |
251 | │ ││ | |
252 | ├─┼┤ | |
253 | ├─┼┤ | |
254 | │ ││ | |
255 | └─┴┘ | |
256 | """ | |
257 | ) | |
258 | ||
259 | MINIMAL: Box = Box( | |
260 | """\ | |
261 | ╷ | |
262 | │ | |
263 | ╶─┼╴ | |
264 | │ | |
265 | ╶─┼╴ | |
266 | ╶─┼╴ | |
267 | │ | |
268 | ╵ | |
269 | """ | |
270 | ) | |
271 | ||
272 | ||
273 | MINIMAL_HEAVY_HEAD: Box = Box( | |
274 | """\ | |
275 | ╷ | |
276 | │ | |
277 | ╺━┿╸ | |
278 | │ | |
279 | ╶─┼╴ | |
280 | ╶─┼╴ | |
281 | │ | |
282 | ╵ | |
283 | """ | |
284 | ) | |
285 | ||
286 | MINIMAL_DOUBLE_HEAD: Box = Box( | |
287 | """\ | |
288 | ╷ | |
289 | │ | |
290 | ═╪ | |
291 | │ | |
292 | ─┼ | |
293 | ─┼ | |
294 | │ | |
295 | ╵ | |
296 | """ | |
297 | ) | |
298 | ||
299 | ||
300 | SIMPLE: Box = Box( | |
301 | """\ | |
302 | ||
303 | ||
304 | ── | |
305 | ||
306 | ||
307 | ── | |
308 | ||
309 | ||
310 | """ | |
311 | ) | |
312 | ||
313 | SIMPLE_HEAD: Box = Box( | |
314 | """\ | |
315 | ||
316 | ||
317 | ── | |
318 | ||
319 | ||
320 | ||
321 | ||
322 | ||
323 | """ | |
324 | ) | |
325 | ||
326 | ||
327 | SIMPLE_HEAVY: Box = Box( | |
328 | """\ | |
329 | ||
330 | ||
331 | ━━ | |
332 | ||
333 | ||
334 | ━━ | |
335 | ||
336 | ||
337 | """ | |
338 | ) | |
339 | ||
340 | ||
341 | HORIZONTALS: Box = Box( | |
342 | """\ | |
343 | ── | |
344 | ||
345 | ── | |
346 | ||
347 | ── | |
348 | ── | |
349 | ||
350 | ── | |
351 | """ | |
352 | ) | |
353 | ||
354 | ROUNDED: Box = Box( | |
355 | """\ | |
356 | ╭─┬╮ | |
357 | │ ││ | |
358 | ├─┼┤ | |
359 | │ ││ | |
360 | ├─┼┤ | |
361 | ├─┼┤ | |
362 | │ ││ | |
363 | ╰─┴╯ | |
364 | """ | |
365 | ) | |
366 | ||
367 | HEAVY: Box = Box( | |
368 | """\ | |
369 | ┏━┳┓ | |
370 | ┃ ┃┃ | |
371 | ┣━╋┫ | |
372 | ┃ ┃┃ | |
373 | ┣━╋┫ | |
374 | ┣━╋┫ | |
375 | ┃ ┃┃ | |
376 | ┗━┻┛ | |
377 | """ | |
378 | ) | |
379 | ||
380 | HEAVY_EDGE: Box = Box( | |
381 | """\ | |
382 | ┏━┯┓ | |
383 | ┃ │┃ | |
384 | ┠─┼┨ | |
385 | ┃ │┃ | |
386 | ┠─┼┨ | |
387 | ┠─┼┨ | |
388 | ┃ │┃ | |
389 | ┗━┷┛ | |
390 | """ | |
391 | ) | |
392 | ||
393 | HEAVY_HEAD: Box = Box( | |
394 | """\ | |
395 | ┏━┳┓ | |
396 | ┃ ┃┃ | |
397 | ┡━╇┩ | |
398 | │ ││ | |
399 | ├─┼┤ | |
400 | ├─┼┤ | |
401 | │ ││ | |
402 | └─┴┘ | |
403 | """ | |
404 | ) | |
405 | ||
406 | DOUBLE: Box = Box( | |
407 | """\ | |
408 | ╔═╦╗ | |
409 | ║ ║║ | |
410 | ╠═╬╣ | |
411 | ║ ║║ | |
412 | ╠═╬╣ | |
413 | ╠═╬╣ | |
414 | ║ ║║ | |
415 | ╚═╩╝ | |
416 | """ | |
417 | ) | |
418 | ||
419 | DOUBLE_EDGE: Box = Box( | |
420 | """\ | |
421 | ╔═╤╗ | |
422 | ║ │║ | |
423 | ╟─┼╢ | |
424 | ║ │║ | |
425 | ╟─┼╢ | |
426 | ╟─┼╢ | |
427 | ║ │║ | |
428 | ╚═╧╝ | |
429 | """ | |
430 | ) | |
431 | ||
432 | MARKDOWN: Box = Box( | |
433 | """\ | |
434 | ||
435 | | || | |
436 | |-|| | |
437 | | || | |
438 | |-|| | |
439 | |-|| | |
440 | | || | |
441 | ||
442 | """, | |
443 | ascii=True, | |
444 | ) | |
445 | ||
446 | # Map Boxes that don't render with raster fonts on to equivalent that do | |
447 | LEGACY_WINDOWS_SUBSTITUTIONS = { | |
448 | ROUNDED: SQUARE, | |
449 | MINIMAL_HEAVY_HEAD: MINIMAL, | |
450 | SIMPLE_HEAVY: SIMPLE, | |
451 | HEAVY: SQUARE, | |
452 | HEAVY_EDGE: SQUARE, | |
453 | HEAVY_HEAD: SQUARE, | |
454 | } | |
455 | ||
456 | # Map headed boxes to their headerless equivalents | |
457 | PLAIN_HEADED_SUBSTITUTIONS = { | |
458 | HEAVY_HEAD: SQUARE, | |
459 | SQUARE_DOUBLE_HEAD: SQUARE, | |
460 | MINIMAL_DOUBLE_HEAD: MINIMAL, | |
461 | MINIMAL_HEAVY_HEAD: MINIMAL, | |
462 | ASCII_DOUBLE_HEAD: ASCII2, | |
463 | } | |
464 | ||
465 | ||
466 | if __name__ == "__main__": # pragma: no cover | |
467 | ||
468 | from pip._vendor.rich.columns import Columns | |
469 | from pip._vendor.rich.panel import Panel | |
470 | ||
471 | from . import box as box | |
472 | from .console import Console | |
473 | from .table import Table | |
474 | from .text import Text | |
475 | ||
476 | console = Console(record=True) | |
477 | ||
478 | BOXES = [ | |
479 | "ASCII", | |
480 | "ASCII2", | |
481 | "ASCII_DOUBLE_HEAD", | |
482 | "SQUARE", | |
483 | "SQUARE_DOUBLE_HEAD", | |
484 | "MINIMAL", | |
485 | "MINIMAL_HEAVY_HEAD", | |
486 | "MINIMAL_DOUBLE_HEAD", | |
487 | "SIMPLE", | |
488 | "SIMPLE_HEAD", | |
489 | "SIMPLE_HEAVY", | |
490 | "HORIZONTALS", | |
491 | "ROUNDED", | |
492 | "HEAVY", | |
493 | "HEAVY_EDGE", | |
494 | "HEAVY_HEAD", | |
495 | "DOUBLE", | |
496 | "DOUBLE_EDGE", | |
497 | "MARKDOWN", | |
498 | ] | |
499 | ||
500 | console.print(Panel("[bold green]Box Constants", style="green"), justify="center") | |
501 | console.print() | |
502 | ||
503 | columns = Columns(expand=True, padding=2) | |
504 | for box_name in sorted(BOXES): | |
505 | table = Table( | |
506 | show_footer=True, style="dim", border_style="not dim", expand=True | |
507 | ) | |
508 | table.add_column("Header 1", "Footer 1") | |
509 | table.add_column("Header 2", "Footer 2") | |
510 | table.add_row("Cell", "Cell") | |
511 | table.add_row("Cell", "Cell") | |
512 | table.box = getattr(box, box_name) | |
513 | table.title = Text(f"box.{box_name}", style="magenta") | |
514 | columns.add_renderable(table) | |
515 | console.print(columns) | |
516 | ||
517 | # console.save_svg("box.svg") |