Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/rich/align.py: 20%

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

127 statements  

1from itertools import chain 

2from typing import TYPE_CHECKING, Iterable, Optional, Literal 

3 

4from .constrain import Constrain 

5from .jupyter import JupyterMixin 

6from .measure import Measurement 

7from .segment import Segment 

8from .style import StyleType 

9 

10if TYPE_CHECKING: 

11 from .console import Console, ConsoleOptions, RenderableType, RenderResult 

12 

13AlignMethod = Literal["left", "center", "right"] 

14VerticalAlignMethod = Literal["top", "middle", "bottom"] 

15 

16 

17class Align(JupyterMixin): 

18 """Align a renderable by adding spaces if necessary. 

19 

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[VerticalAlignMethod], 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 

29 Raises: 

30 ValueError: if ``align`` is not one of the expected values. 

31 """ 

32 

33 def __init__( 

34 self, 

35 renderable: "RenderableType", 

36 align: AlignMethod = "left", 

37 style: Optional[StyleType] = None, 

38 *, 

39 vertical: Optional[VerticalAlignMethod] = None, 

40 pad: bool = True, 

41 width: Optional[int] = None, 

42 height: Optional[int] = None, 

43 ) -> None: 

44 if align not in ("left", "center", "right"): 

45 raise ValueError( 

46 f'invalid value for align, expected "left", "center", or "right" (not {align!r})' 

47 ) 

48 if vertical is not None and vertical not in ("top", "middle", "bottom"): 

49 raise ValueError( 

50 f'invalid value for vertical, expected "top", "middle", or "bottom" (not {vertical!r})' 

51 ) 

52 self.renderable = renderable 

53 self.align = align 

54 self.style = style 

55 self.vertical = vertical 

56 self.pad = pad 

57 self.width = width 

58 self.height = height 

59 

60 def __repr__(self) -> str: 

61 return f"Align({self.renderable!r}, {self.align!r})" 

62 

63 @classmethod 

64 def left( 

65 cls, 

66 renderable: "RenderableType", 

67 style: Optional[StyleType] = None, 

68 *, 

69 vertical: Optional[VerticalAlignMethod] = None, 

70 pad: bool = True, 

71 width: Optional[int] = None, 

72 height: Optional[int] = None, 

73 ) -> "Align": 

74 """Align a renderable to the left.""" 

75 return cls( 

76 renderable, 

77 "left", 

78 style=style, 

79 vertical=vertical, 

80 pad=pad, 

81 width=width, 

82 height=height, 

83 ) 

84 

85 @classmethod 

86 def center( 

87 cls, 

88 renderable: "RenderableType", 

89 style: Optional[StyleType] = None, 

90 *, 

91 vertical: Optional[VerticalAlignMethod] = None, 

92 pad: bool = True, 

93 width: Optional[int] = None, 

94 height: Optional[int] = None, 

95 ) -> "Align": 

96 """Align a renderable to the center.""" 

97 return cls( 

98 renderable, 

99 "center", 

100 style=style, 

101 vertical=vertical, 

102 pad=pad, 

103 width=width, 

104 height=height, 

105 ) 

106 

107 @classmethod 

108 def right( 

109 cls, 

110 renderable: "RenderableType", 

111 style: Optional[StyleType] = None, 

112 *, 

113 vertical: Optional[VerticalAlignMethod] = None, 

114 pad: bool = True, 

115 width: Optional[int] = None, 

116 height: Optional[int] = None, 

117 ) -> "Align": 

118 """Align a renderable to the right.""" 

119 return cls( 

120 renderable, 

121 "right", 

122 style=style, 

123 vertical=vertical, 

124 pad=pad, 

125 width=width, 

126 height=height, 

127 ) 

128 

129 def __rich_console__( 

130 self, console: "Console", options: "ConsoleOptions" 

131 ) -> "RenderResult": 

132 align = self.align 

133 width = console.measure(self.renderable, options=options).maximum 

134 rendered = console.render( 

135 Constrain( 

136 self.renderable, width if self.width is None else min(width, self.width) 

137 ), 

138 options.update(height=None), 

139 ) 

140 lines = list(Segment.split_lines(rendered)) 

141 width, height = Segment.get_shape(lines) 

142 lines = Segment.set_shape(lines, width, height) 

143 new_line = Segment.line() 

144 excess_space = options.max_width - width 

145 style = console.get_style(self.style) if self.style is not None else None 

146 

147 def generate_segments() -> Iterable[Segment]: 

148 if excess_space <= 0: 

149 # Exact fit 

150 for line in lines: 

151 yield from line 

152 yield new_line 

153 

154 elif align == "left": 

155 # Pad on the right 

156 pad = Segment(" " * excess_space, style) if self.pad else None 

157 for line in lines: 

158 yield from line 

159 if pad: 

160 yield pad 

161 yield new_line 

162 

163 elif align == "center": 

164 # Pad left and right 

165 left = excess_space // 2 

166 pad = Segment(" " * left, style) 

167 pad_right = ( 

168 Segment(" " * (excess_space - left), style) if self.pad else None 

169 ) 

170 for line in lines: 

171 if left: 

172 yield pad 

173 yield from line 

174 if pad_right: 

175 yield pad_right 

176 yield new_line 

177 

178 elif align == "right": 

179 # Padding on left 

180 pad = Segment(" " * excess_space, style) 

181 for line in lines: 

182 yield pad 

183 yield from line 

184 yield new_line 

185 

186 blank_line = ( 

187 Segment(f"{' ' * (self.width or options.max_width)}\n", style) 

188 if self.pad 

189 else Segment("\n") 

190 ) 

191 

192 def blank_lines(count: int) -> Iterable[Segment]: 

193 if count > 0: 

194 for _ in range(count): 

195 yield blank_line 

196 

197 vertical_height = self.height or options.height 

198 iter_segments: Iterable[Segment] 

199 if self.vertical and vertical_height is not None: 

200 if self.vertical == "top": 

201 bottom_space = vertical_height - height 

202 iter_segments = chain(generate_segments(), blank_lines(bottom_space)) 

203 elif self.vertical == "middle": 

204 top_space = (vertical_height - height) // 2 

205 bottom_space = vertical_height - top_space - height 

206 iter_segments = chain( 

207 blank_lines(top_space), 

208 generate_segments(), 

209 blank_lines(bottom_space), 

210 ) 

211 else: # self.vertical == "bottom": 

212 top_space = vertical_height - height 

213 iter_segments = chain(blank_lines(top_space), generate_segments()) 

214 else: 

215 iter_segments = generate_segments() 

216 if self.style: 

217 style = console.get_style(self.style) 

218 iter_segments = Segment.apply_style(iter_segments, style) 

219 yield from iter_segments 

220 

221 def __rich_measure__( 

222 self, console: "Console", options: "ConsoleOptions" 

223 ) -> Measurement: 

224 measurement = Measurement.get(console, options, self.renderable) 

225 return measurement 

226 

227 

228class VerticalCenter(JupyterMixin): 

229 """Vertically aligns a renderable. 

230 

231 Warn: 

232 This class is deprecated and may be removed in a future version. Use Align class with 

233 `vertical="middle"`. 

234 

235 Args: 

236 renderable (RenderableType): A renderable object. 

237 style (StyleType, optional): An optional style to apply to the background. Defaults to None. 

238 """ 

239 

240 def __init__( 

241 self, 

242 renderable: "RenderableType", 

243 style: Optional[StyleType] = None, 

244 ) -> None: 

245 self.renderable = renderable 

246 self.style = style 

247 

248 def __repr__(self) -> str: 

249 return f"VerticalCenter({self.renderable!r})" 

250 

251 def __rich_console__( 

252 self, console: "Console", options: "ConsoleOptions" 

253 ) -> "RenderResult": 

254 style = console.get_style(self.style) if self.style is not None else None 

255 lines = console.render_lines( 

256 self.renderable, options.update(height=None), pad=False 

257 ) 

258 width, _height = Segment.get_shape(lines) 

259 new_line = Segment.line() 

260 height = options.height or options.size.height 

261 top_space = (height - len(lines)) // 2 

262 bottom_space = height - top_space - len(lines) 

263 blank_line = Segment(f"{' ' * width}", style) 

264 

265 def blank_lines(count: int) -> Iterable[Segment]: 

266 for _ in range(count): 

267 yield blank_line 

268 yield new_line 

269 

270 if top_space > 0: 

271 yield from blank_lines(top_space) 

272 for line in lines: 

273 yield from line 

274 yield new_line 

275 if bottom_space > 0: 

276 yield from blank_lines(bottom_space) 

277 

278 def __rich_measure__( 

279 self, console: "Console", options: "ConsoleOptions" 

280 ) -> Measurement: 

281 measurement = Measurement.get(console, options, self.renderable) 

282 return measurement 

283 

284 

285if __name__ == "__main__": # pragma: no cover 

286 from rich.console import Console, Group 

287 from rich.highlighter import ReprHighlighter 

288 from rich.panel import Panel 

289 

290 highlighter = ReprHighlighter() 

291 console = Console() 

292 

293 panel = Panel( 

294 Group( 

295 Align.left(highlighter("align='left'")), 

296 Align.center(highlighter("align='center'")), 

297 Align.right(highlighter("align='right'")), 

298 ), 

299 width=60, 

300 style="on dark_blue", 

301 title="Align", 

302 ) 

303 

304 console.print( 

305 Align.center(panel, vertical="middle", style="on red", height=console.height) 

306 )