align.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. import sys
  2. from itertools import chain
  3. from typing import TYPE_CHECKING, Iterable, Optional
  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. from .constrain import Constrain
  9. from .jupyter import JupyterMixin
  10. from .measure import Measurement
  11. from .segment import Segment
  12. from .style import StyleType
  13. if TYPE_CHECKING:
  14. from .console import Console, ConsoleOptions, RenderableType, RenderResult
  15. AlignMethod = Literal["left", "center", "right"]
  16. VerticalAlignMethod = Literal["top", "middle", "bottom"]
  17. AlignValues = AlignMethod # TODO: deprecate AlignValues
  18. class Align(JupyterMixin):
  19. """Align a renderable by adding spaces if necessary.
  20. Args:
  21. renderable (RenderableType): A console renderable.
  22. align (AlignMethod): One of "left", "center", or "right""
  23. style (StyleType, optional): An optional style to apply to the background.
  24. vertical (Optional[VerticalAlginMethod], optional): Optional vertical align, one of "top", "middle", or "bottom". Defaults to None.
  25. pad (bool, optional): Pad the right with spaces. Defaults to True.
  26. width (int, optional): Restrict contents to given width, or None to use default width. Defaults to None.
  27. height (int, optional): Set height of align renderable, or None to fit to contents. Defaults to None.
  28. Raises:
  29. ValueError: if ``align`` is not one of the expected values.
  30. """
  31. def __init__(
  32. self,
  33. renderable: "RenderableType",
  34. align: AlignMethod = "left",
  35. style: Optional[StyleType] = None,
  36. *,
  37. vertical: Optional[VerticalAlignMethod] = None,
  38. pad: bool = True,
  39. width: Optional[int] = None,
  40. height: Optional[int] = None,
  41. ) -> None:
  42. if align not in ("left", "center", "right"):
  43. raise ValueError(
  44. f'invalid value for align, expected "left", "center", or "right" (not {align!r})'
  45. )
  46. if vertical is not None and vertical not in ("top", "middle", "bottom"):
  47. raise ValueError(
  48. f'invalid value for vertical, expected "top", "middle", or "bottom" (not {vertical!r})'
  49. )
  50. self.renderable = renderable
  51. self.align = align
  52. self.style = style
  53. self.vertical = vertical
  54. self.pad = pad
  55. self.width = width
  56. self.height = height
  57. def __repr__(self) -> str:
  58. return f"Align({self.renderable!r}, {self.align!r})"
  59. @classmethod
  60. def left(
  61. cls,
  62. renderable: "RenderableType",
  63. style: Optional[StyleType] = None,
  64. *,
  65. vertical: Optional[VerticalAlignMethod] = None,
  66. pad: bool = True,
  67. width: Optional[int] = None,
  68. height: Optional[int] = None,
  69. ) -> "Align":
  70. """Align a renderable to the left."""
  71. return cls(
  72. renderable,
  73. "left",
  74. style=style,
  75. vertical=vertical,
  76. pad=pad,
  77. width=width,
  78. height=height,
  79. )
  80. @classmethod
  81. def center(
  82. cls,
  83. renderable: "RenderableType",
  84. style: Optional[StyleType] = None,
  85. *,
  86. vertical: Optional[VerticalAlignMethod] = None,
  87. pad: bool = True,
  88. width: Optional[int] = None,
  89. height: Optional[int] = None,
  90. ) -> "Align":
  91. """Align a renderable to the center."""
  92. return cls(
  93. renderable,
  94. "center",
  95. style=style,
  96. vertical=vertical,
  97. pad=pad,
  98. width=width,
  99. height=height,
  100. )
  101. @classmethod
  102. def right(
  103. cls,
  104. renderable: "RenderableType",
  105. style: Optional[StyleType] = None,
  106. *,
  107. vertical: Optional[VerticalAlignMethod] = None,
  108. pad: bool = True,
  109. width: Optional[int] = None,
  110. height: Optional[int] = None,
  111. ) -> "Align":
  112. """Align a renderable to the right."""
  113. return cls(
  114. renderable,
  115. "right",
  116. style=style,
  117. vertical=vertical,
  118. pad=pad,
  119. width=width,
  120. height=height,
  121. )
  122. def __rich_console__(
  123. self, console: "Console", options: "ConsoleOptions"
  124. ) -> "RenderResult":
  125. align = self.align
  126. width = console.measure(self.renderable, options=options).maximum
  127. rendered = console.render(
  128. Constrain(
  129. self.renderable, width if self.width is None else min(width, self.width)
  130. ),
  131. options.update(height=None),
  132. )
  133. lines = list(Segment.split_lines(rendered))
  134. width, height = Segment.get_shape(lines)
  135. lines = Segment.set_shape(lines, width, height)
  136. new_line = Segment.line()
  137. excess_space = options.max_width - width
  138. style = console.get_style(self.style) if self.style is not None else None
  139. def generate_segments() -> Iterable[Segment]:
  140. if excess_space <= 0:
  141. # Exact fit
  142. for line in lines:
  143. yield from line
  144. yield new_line
  145. elif align == "left":
  146. # Pad on the right
  147. pad = Segment(" " * excess_space, style) if self.pad else None
  148. for line in lines:
  149. yield from line
  150. if pad:
  151. yield pad
  152. yield new_line
  153. elif align == "center":
  154. # Pad left and right
  155. left = excess_space // 2
  156. pad = Segment(" " * left, style)
  157. pad_right = (
  158. Segment(" " * (excess_space - left), style) if self.pad else None
  159. )
  160. for line in lines:
  161. if left:
  162. yield pad
  163. yield from line
  164. if pad_right:
  165. yield pad_right
  166. yield new_line
  167. elif align == "right":
  168. # Padding on left
  169. pad = Segment(" " * excess_space, style)
  170. for line in lines:
  171. yield pad
  172. yield from line
  173. yield new_line
  174. blank_line = (
  175. Segment(f"{' ' * (self.width or options.max_width)}\n", style)
  176. if self.pad
  177. else Segment("\n")
  178. )
  179. def blank_lines(count: int) -> Iterable[Segment]:
  180. if count > 0:
  181. for _ in range(count):
  182. yield blank_line
  183. vertical_height = self.height or options.height
  184. iter_segments: Iterable[Segment]
  185. if self.vertical and vertical_height is not None:
  186. if self.vertical == "top":
  187. bottom_space = vertical_height - height
  188. iter_segments = chain(generate_segments(), blank_lines(bottom_space))
  189. elif self.vertical == "middle":
  190. top_space = (vertical_height - height) // 2
  191. bottom_space = vertical_height - top_space - height
  192. iter_segments = chain(
  193. blank_lines(top_space),
  194. generate_segments(),
  195. blank_lines(bottom_space),
  196. )
  197. else: # self.vertical == "bottom":
  198. top_space = vertical_height - height
  199. iter_segments = chain(blank_lines(top_space), generate_segments())
  200. else:
  201. iter_segments = generate_segments()
  202. if self.style:
  203. style = console.get_style(self.style)
  204. iter_segments = Segment.apply_style(iter_segments, style)
  205. yield from iter_segments
  206. def __rich_measure__(
  207. self, console: "Console", options: "ConsoleOptions"
  208. ) -> Measurement:
  209. measurement = Measurement.get(console, options, self.renderable)
  210. return measurement
  211. class VerticalCenter(JupyterMixin):
  212. """Vertically aligns a renderable.
  213. Warn:
  214. This class is deprecated and may be removed in a future version. Use Align class with
  215. `vertical="middle"`.
  216. Args:
  217. renderable (RenderableType): A renderable object.
  218. """
  219. def __init__(
  220. self,
  221. renderable: "RenderableType",
  222. style: Optional[StyleType] = None,
  223. ) -> None:
  224. self.renderable = renderable
  225. self.style = style
  226. def __repr__(self) -> str:
  227. return f"VerticalCenter({self.renderable!r})"
  228. def __rich_console__(
  229. self, console: "Console", options: "ConsoleOptions"
  230. ) -> "RenderResult":
  231. style = console.get_style(self.style) if self.style is not None else None
  232. lines = console.render_lines(
  233. self.renderable, options.update(height=None), pad=False
  234. )
  235. width, _height = Segment.get_shape(lines)
  236. new_line = Segment.line()
  237. height = options.height or options.size.height
  238. top_space = (height - len(lines)) // 2
  239. bottom_space = height - top_space - len(lines)
  240. blank_line = Segment(f"{' ' * width}", style)
  241. def blank_lines(count: int) -> Iterable[Segment]:
  242. for _ in range(count):
  243. yield blank_line
  244. yield new_line
  245. if top_space > 0:
  246. yield from blank_lines(top_space)
  247. for line in lines:
  248. yield from line
  249. yield new_line
  250. if bottom_space > 0:
  251. yield from blank_lines(bottom_space)
  252. def __rich_measure__(
  253. self, console: "Console", options: "ConsoleOptions"
  254. ) -> Measurement:
  255. measurement = Measurement.get(console, options, self.renderable)
  256. return measurement
  257. if __name__ == "__main__": # pragma: no cover
  258. from pip._vendor.rich.console import Console, Group
  259. from pip._vendor.rich.highlighter import ReprHighlighter
  260. from pip._vendor.rich.panel import Panel
  261. highlighter = ReprHighlighter()
  262. console = Console()
  263. panel = Panel(
  264. Group(
  265. Align.left(highlighter("align='left'")),
  266. Align.center(highlighter("align='center'")),
  267. Align.right(highlighter("align='right'")),
  268. ),
  269. width=60,
  270. style="on dark_blue",
  271. title="Algin",
  272. )
  273. console.print(
  274. Align.center(panel, vertical="middle", style="on red", height=console.height)
  275. )