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