box.py 8.9 KB

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