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