box.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  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_plain_headed_box(self) -> "Box":
  76. """If this box uses special characters for the borders of the header, then
  77. return the equivalent box that does not.
  78. Returns:
  79. Box: The most similar Box that doesn't use header-specific box characters.
  80. If the current Box already satisfies this criterion, then it's returned.
  81. """
  82. return PLAIN_HEADED_SUBSTITUTIONS.get(self, self)
  83. def get_top(self, widths: Iterable[int]) -> str:
  84. """Get the top of a simple box.
  85. Args:
  86. widths (List[int]): Widths of columns.
  87. Returns:
  88. str: A string of box characters.
  89. """
  90. parts: List[str] = []
  91. append = parts.append
  92. append(self.top_left)
  93. for last, width in loop_last(widths):
  94. append(self.top * width)
  95. if not last:
  96. append(self.top_divider)
  97. append(self.top_right)
  98. return "".join(parts)
  99. def get_row(
  100. self,
  101. widths: Iterable[int],
  102. level: Literal["head", "row", "foot", "mid"] = "row",
  103. edge: bool = True,
  104. ) -> str:
  105. """Get the top of a simple box.
  106. Args:
  107. width (List[int]): Widths of columns.
  108. Returns:
  109. str: A string of box characters.
  110. """
  111. if level == "head":
  112. left = self.head_row_left
  113. horizontal = self.head_row_horizontal
  114. cross = self.head_row_cross
  115. right = self.head_row_right
  116. elif level == "row":
  117. left = self.row_left
  118. horizontal = self.row_horizontal
  119. cross = self.row_cross
  120. right = self.row_right
  121. elif level == "mid":
  122. left = self.mid_left
  123. horizontal = " "
  124. cross = self.mid_vertical
  125. right = self.mid_right
  126. elif level == "foot":
  127. left = self.foot_row_left
  128. horizontal = self.foot_row_horizontal
  129. cross = self.foot_row_cross
  130. right = self.foot_row_right
  131. else:
  132. raise ValueError("level must be 'head', 'row' or 'foot'")
  133. parts: List[str] = []
  134. append = parts.append
  135. if edge:
  136. append(left)
  137. for last, width in loop_last(widths):
  138. append(horizontal * width)
  139. if not last:
  140. append(cross)
  141. if edge:
  142. append(right)
  143. return "".join(parts)
  144. def get_bottom(self, widths: Iterable[int]) -> str:
  145. """Get the bottom of a simple box.
  146. Args:
  147. widths (List[int]): Widths of columns.
  148. Returns:
  149. str: A string of box characters.
  150. """
  151. parts: List[str] = []
  152. append = parts.append
  153. append(self.bottom_left)
  154. for last, width in loop_last(widths):
  155. append(self.bottom * width)
  156. if not last:
  157. append(self.bottom_divider)
  158. append(self.bottom_right)
  159. return "".join(parts)
  160. ASCII: Box = Box(
  161. """\
  162. +--+
  163. | ||
  164. |-+|
  165. | ||
  166. |-+|
  167. |-+|
  168. | ||
  169. +--+
  170. """,
  171. ascii=True,
  172. )
  173. ASCII2: Box = Box(
  174. """\
  175. +-++
  176. | ||
  177. +-++
  178. | ||
  179. +-++
  180. +-++
  181. | ||
  182. +-++
  183. """,
  184. ascii=True,
  185. )
  186. ASCII_DOUBLE_HEAD: Box = Box(
  187. """\
  188. +-++
  189. | ||
  190. +=++
  191. | ||
  192. +-++
  193. +-++
  194. | ||
  195. +-++
  196. """,
  197. ascii=True,
  198. )
  199. SQUARE: Box = Box(
  200. """\
  201. ┌─┬┐
  202. │ ││
  203. ├─┼┤
  204. │ ││
  205. ├─┼┤
  206. ├─┼┤
  207. │ ││
  208. └─┴┘
  209. """
  210. )
  211. SQUARE_DOUBLE_HEAD: Box = Box(
  212. """\
  213. ┌─┬┐
  214. │ ││
  215. ╞═╪╡
  216. │ ││
  217. ├─┼┤
  218. ├─┼┤
  219. │ ││
  220. └─┴┘
  221. """
  222. )
  223. MINIMAL: Box = Box(
  224. """\
  225. ╶─┼╴
  226. ╶─┼╴
  227. ╶─┼╴
  228. """
  229. )
  230. MINIMAL_HEAVY_HEAD: Box = Box(
  231. """\
  232. ╺━┿╸
  233. ╶─┼╴
  234. ╶─┼╴
  235. """
  236. )
  237. MINIMAL_DOUBLE_HEAD: Box = Box(
  238. """\
  239. ═╪
  240. ─┼
  241. ─┼
  242. """
  243. )
  244. SIMPLE: Box = Box(
  245. """\
  246. ──
  247. ──
  248. """
  249. )
  250. SIMPLE_HEAD: Box = Box(
  251. """\
  252. ──
  253. """
  254. )
  255. SIMPLE_HEAVY: Box = Box(
  256. """\
  257. ━━
  258. ━━
  259. """
  260. )
  261. HORIZONTALS: Box = Box(
  262. """\
  263. ──
  264. ──
  265. ──
  266. ──
  267. ──
  268. """
  269. )
  270. ROUNDED: Box = Box(
  271. """\
  272. ╭─┬╮
  273. │ ││
  274. ├─┼┤
  275. │ ││
  276. ├─┼┤
  277. ├─┼┤
  278. │ ││
  279. ╰─┴╯
  280. """
  281. )
  282. HEAVY: Box = Box(
  283. """\
  284. ┏━┳┓
  285. ┃ ┃┃
  286. ┣━╋┫
  287. ┃ ┃┃
  288. ┣━╋┫
  289. ┣━╋┫
  290. ┃ ┃┃
  291. ┗━┻┛
  292. """
  293. )
  294. HEAVY_EDGE: Box = Box(
  295. """\
  296. ┏━┯┓
  297. ┃ │┃
  298. ┠─┼┨
  299. ┃ │┃
  300. ┠─┼┨
  301. ┠─┼┨
  302. ┃ │┃
  303. ┗━┷┛
  304. """
  305. )
  306. HEAVY_HEAD: Box = Box(
  307. """\
  308. ┏━┳┓
  309. ┃ ┃┃
  310. ┡━╇┩
  311. │ ││
  312. ├─┼┤
  313. ├─┼┤
  314. │ ││
  315. └─┴┘
  316. """
  317. )
  318. DOUBLE: Box = Box(
  319. """\
  320. ╔═╦╗
  321. ║ ║║
  322. ╠═╬╣
  323. ║ ║║
  324. ╠═╬╣
  325. ╠═╬╣
  326. ║ ║║
  327. ╚═╩╝
  328. """
  329. )
  330. DOUBLE_EDGE: Box = Box(
  331. """\
  332. ╔═╤╗
  333. ║ │║
  334. ╟─┼╢
  335. ║ │║
  336. ╟─┼╢
  337. ╟─┼╢
  338. ║ │║
  339. ╚═╧╝
  340. """
  341. )
  342. MARKDOWN: Box = Box(
  343. """\
  344. | ||
  345. |-||
  346. | ||
  347. |-||
  348. |-||
  349. | ||
  350. """,
  351. ascii=True,
  352. )
  353. # Map Boxes that don't render with raster fonts on to equivalent that do
  354. LEGACY_WINDOWS_SUBSTITUTIONS = {
  355. ROUNDED: SQUARE,
  356. MINIMAL_HEAVY_HEAD: MINIMAL,
  357. SIMPLE_HEAVY: SIMPLE,
  358. HEAVY: SQUARE,
  359. HEAVY_EDGE: SQUARE,
  360. HEAVY_HEAD: SQUARE,
  361. }
  362. # Map headed boxes to their headerless equivalents
  363. PLAIN_HEADED_SUBSTITUTIONS = {
  364. HEAVY_HEAD: SQUARE,
  365. SQUARE_DOUBLE_HEAD: SQUARE,
  366. MINIMAL_DOUBLE_HEAD: MINIMAL,
  367. MINIMAL_HEAVY_HEAD: MINIMAL,
  368. ASCII_DOUBLE_HEAD: ASCII2,
  369. }
  370. if __name__ == "__main__": # pragma: no cover
  371. from pip._vendor.rich.columns import Columns
  372. from pip._vendor.rich.panel import Panel
  373. from . import box as box
  374. from .console import Console
  375. from .table import Table
  376. from .text import Text
  377. console = Console(record=True)
  378. BOXES = [
  379. "ASCII",
  380. "ASCII2",
  381. "ASCII_DOUBLE_HEAD",
  382. "SQUARE",
  383. "SQUARE_DOUBLE_HEAD",
  384. "MINIMAL",
  385. "MINIMAL_HEAVY_HEAD",
  386. "MINIMAL_DOUBLE_HEAD",
  387. "SIMPLE",
  388. "SIMPLE_HEAD",
  389. "SIMPLE_HEAVY",
  390. "HORIZONTALS",
  391. "ROUNDED",
  392. "HEAVY",
  393. "HEAVY_EDGE",
  394. "HEAVY_HEAD",
  395. "DOUBLE",
  396. "DOUBLE_EDGE",
  397. "MARKDOWN",
  398. ]
  399. console.print(Panel("[bold green]Box Constants", style="green"), justify="center")
  400. console.print()
  401. columns = Columns(expand=True, padding=2)
  402. for box_name in sorted(BOXES):
  403. table = Table(
  404. show_footer=True, style="dim", border_style="not dim", expand=True
  405. )
  406. table.add_column("Header 1", "Footer 1")
  407. table.add_column("Header 2", "Footer 2")
  408. table.add_row("Cell", "Cell")
  409. table.add_row("Cell", "Cell")
  410. table.box = getattr(box, box_name)
  411. table.title = Text(f"box.{box_name}", style="magenta")
  412. columns.add_renderable(table)
  413. console.print(columns)
  414. # console.save_svg("box.svg")