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

1import sys 

2from itertools import chain 

3from typing import TYPE_CHECKING, Iterable, Optional 

4 

5if sys.version_info >= (3, 8): 

6 from typing import Literal 

7else: 

8 from typing_extensions import Literal # pragma: no cover 

9 

10from .constrain import Constrain 

11from .jupyter import JupyterMixin 

12from .measure import Measurement 

13from .segment import Segment 

14from .style import StyleType 

15 

16if TYPE_CHECKING: 

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

18 

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

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

21 

22 

23class Align(JupyterMixin): 

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

25 

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. 

34 

35 Raises: 

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

37 """ 

38 

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 

65 

66 def __repr__(self) -> str: 

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

68 

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 ) 

90 

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 ) 

112 

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 ) 

134 

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 

152 

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 

159 

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 

168 

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 

183 

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 

191 

192 blank_line = ( 

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

194 if self.pad 

195 else Segment("\n") 

196 ) 

197 

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

199 if count > 0: 

200 for _ in range(count): 

201 yield blank_line 

202 

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 

226 

227 def __rich_measure__( 

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

229 ) -> Measurement: 

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

231 return measurement 

232 

233 

234class VerticalCenter(JupyterMixin): 

235 """Vertically aligns a renderable. 

236 

237 Warn: 

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

239 `vertical="middle"`. 

240 

241 Args: 

242 renderable (RenderableType): A renderable object. 

243 """ 

244 

245 def __init__( 

246 self, 

247 renderable: "RenderableType", 

248 style: Optional[StyleType] = None, 

249 ) -> None: 

250 self.renderable = renderable 

251 self.style = style 

252 

253 def __repr__(self) -> str: 

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

255 

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) 

269 

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

271 for _ in range(count): 

272 yield blank_line 

273 yield new_line 

274 

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) 

282 

283 def __rich_measure__( 

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

285 ) -> Measurement: 

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

287 return measurement 

288 

289 

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 

294 

295 highlighter = ReprHighlighter() 

296 console = Console() 

297 

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 ) 

308 

309 console.print( 

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

311 )