Coverage for src/appl/compositor.py: 92%

114 statements  

« prev     ^ index     » next       coverage.py v7.6.7, created at 2024-11-22 15:39 -0800

1"""Containg the compositor classes. 

2 

3All examples shows the composed prompt in APPL functions. 

4""" 

5 

6from __future__ import annotations 

7 

8from types import TracebackType 

9from typing import Any, Dict, Iterable, Optional, Union 

10 

11from .const import INDENT4 as INDENT 

12from .core import ApplStr, Compositor, Indexing, PromptContext 

13from .func import need_ctx 

14 

15 

16class LineSeparated(Compositor): 

17 r"""The line separated compositor. 

18 

19 Attributes: 

20 _sep: The class default separator is "\n". 

21 

22 Example: 

23 ```py 

24 >>> with LineSeparated(): 

25 ... "item1" 

26 ... "item2" 

27 <<< The prompt will be: 

28 item1 

29 item2 

30 ``` 

31 """ 

32 

33 _sep = "\n" 

34 

35 

36class DoubleLineSeparated(Compositor): 

37 r"""The double line separated compositor. 

38 

39 Attributes: 

40 _sep: The class default separator is "\n\n". 

41 

42 Example: 

43 ```py 

44 >>> with DoubleLineSeparated(): 

45 ... "item1" 

46 ... "item2" 

47 <<< The prompt will be: 

48 item1 

49 

50 item2 

51 ``` 

52 """ 

53 

54 _sep = "\n\n" 

55 

56 

57class NoIndent(LineSeparated): 

58 """The list compositor with no indentation. 

59 

60 Attributes: 

61 _inc_indent: The class default indentation is "". 

62 

63 Example: 

64 ```py 

65 >>> with IndentedList(): 

66 ... with NoIndent(): 

67 ... "item1" 

68 ... "item2" 

69 <<< The prompt will be: 

70 item1 

71 item2 

72 ``` 

73 """ 

74 

75 _new_indent = "" 

76 

77 

78class IndentedList(LineSeparated): 

79 """The indented list compositor. 

80 

81 Attributes: 

82 _inc_indent: The class default indentation is INDENT. 

83 

84 Example: 

85 ```py 

86 >>> "BEGIN" 

87 ... with IndentedList(): 

88 ... "item1" 

89 ... "item2" 

90 <<< The prompt will be: 

91 BEGIN 

92 item1 

93 item2 

94 ``` 

95 """ 

96 

97 _inc_indent = INDENT 

98 

99 

100class NumberedList(LineSeparated): 

101 """The number list compositor. 

102 

103 Attributes: 

104 _indexing: The class default indexing mode is "number". 

105 

106 Example: 

107 ```py 

108 >>> with NumberedList(): 

109 ... "item1" 

110 ... "item2" 

111 <<< The prompt will be: 

112 1. item1 

113 2. item2 

114 ``` 

115 """ 

116 

117 _indexing = Indexing("number") 

118 

119 

120class LowerLetterList(LineSeparated): 

121 """The lower letter list compositor. 

122 

123 Attributes: 

124 _indexing: The class default indexing mode is "lower". 

125 

126 Example: 

127 ```py 

128 >>> with LowerLetterList(): 

129 ... "item1" 

130 ... "item2" 

131 <<< The prompt will be: 

132 a. item1 

133 b. item2 

134 ``` 

135 """ 

136 

137 _indexing = Indexing("lower") 

138 

139 

140class UpperLetterList(LineSeparated): 

141 """The upper letter list compositor. 

142 

143 Attributes: 

144 _indexing: The class default indexing mode is "upper". 

145 

146 Example: 

147 ```py 

148 >>> with UpperLetterList(): 

149 ... "item1" 

150 ... "item2" 

151 <<< The prompt will be: 

152 A. item1 

153 B. item2 

154 ``` 

155 """ 

156 

157 _indexing = Indexing("upper") 

158 

159 

160class LowerRomanList(LineSeparated): 

161 """The lower roman list compositor. 

162 

163 Attributes: 

164 _indexing: The class default indexing mode is "roman". 

165 

166 Example: 

167 ```py 

168 >>> with LowerRomanList(): 

169 ... "item1" 

170 ... "item2" 

171 <<< The prompt will be: 

172 i. item1 

173 ii. item2 

174 ``` 

175 """ 

176 

177 _indexing = Indexing("roman") 

178 

179 

180class UpperRomanList(LineSeparated): 

181 """The upper roman list compositor. 

182 

183 Attributes: 

184 _indexing: The class default indexing mode is "Roman". 

185 

186 Example: 

187 ```py 

188 >>> with UpperRomanList(): 

189 ... "item1" 

190 ... "item2" 

191 <<< The prompt will be: 

192 I. item1 

193 II. item2 

194 ``` 

195 """ 

196 

197 _indexing = Indexing("Roman") 

198 

199 

200class DashList(LineSeparated): 

201 """The dash list compositor. 

202 

203 Attributes: 

204 _indexing: The class default indexing mode is "dash". 

205 

206 Example: 

207 ```py 

208 >>> with DashList(): 

209 ... "item1" 

210 ... "item2" 

211 <<< The prompt will be: 

212 - item1 

213 - item2 

214 ``` 

215 """ 

216 

217 _indexing = Indexing("dash") 

218 

219 

220class StarList(LineSeparated): 

221 """The star list compositor. 

222 

223 Attributes: 

224 _indexing: The class default indexing mode is "star". 

225 

226 Example: 

227 ```py 

228 >>> with StarList(): 

229 ... "item1" 

230 ... "item2" 

231 <<< The prompt will be: 

232 * item1 

233 * item2 

234 ``` 

235 """ 

236 

237 _indexing = Indexing("star") 

238 

239 

240LetterList = UpperLetterList 

241"""The alias of UpperLetterList.""" 

242RomanList = UpperRomanList 

243"""The alias of UpperRomanList.""" 

244 

245 

246class Logged(LineSeparated): 

247 """The logged compositor, which is used to wrap the content with logs. 

248 

249 Note the indent will also apply to the prolog and epilog. 

250 

251 Attributes: 

252 _indent_inside: 

253 The class default indentation inside prolog and epilog is "". 

254 

255 Example: 

256 ```py 

257 >>> with Logged(prolog="BEGIN", epilog="END"): 

258 ... "item1" 

259 ... "item2" 

260 <<< The prompt will be: 

261 BEGIN 

262 item1 

263 item2 

264 END 

265 ``` 

266 """ 

267 

268 _indent_inside: Optional[str] = "" 

269 

270 def __init__( 

271 self, 

272 *args: Any, 

273 prolog: str, 

274 epilog: str, 

275 indent_inside: Union[str, int, None] = None, 

276 **kwargs: Any, 

277 ) -> None: 

278 """Initialize the logged compositor. 

279 

280 Args: 

281 *args: The arguments. 

282 prolog: The prolog string. 

283 epilog: The epilog string. 

284 indent_inside: The indentation inside the prolog and epilog. 

285 **kwargs: The keyword arguments. 

286 """ 

287 self._prolog = prolog 

288 self._epilog = epilog 

289 if isinstance(indent_inside, int): 

290 indent_inside = " " * indent_inside 

291 if indent_inside is not None: 

292 if self._indent_inside is None: 

293 raise ValueError( 

294 "Indentation inside is not allowed for this compositor." 

295 ) 

296 self._indent_inside = indent_inside 

297 outer_indent = kwargs.pop("indent", None) 

298 super().__init__(indent=outer_indent, _ctx=kwargs.get("_ctx")) 

299 kwargs = self._get_kwargs_for_inner(kwargs) 

300 # The arguments are passed to the inner compositor 

301 self._indent_compositor = LineSeparated(*args, **kwargs) 

302 

303 @property 

304 def prolog(self) -> str: 

305 """The prolog string.""" 

306 return self._prolog 

307 

308 @property 

309 def epilog(self) -> str: 

310 """The epilog string.""" 

311 return self._epilog 

312 

313 def _get_kwargs_for_inner(self, kwargs: Dict[str, Any]) -> Dict[str, Any]: 

314 kwargs["indent"] = self._indent_inside 

315 return kwargs 

316 

317 def _enter(self) -> None: 

318 super()._enter() 

319 if self._ctx is not None: 

320 self._ctx.add_string(self.prolog) 

321 self._indent_compositor.__enter__() 

322 

323 def _exit( 

324 self, 

325 _exc_type: Optional[type[BaseException]], 

326 _exc_value: Optional[BaseException], 

327 _traceback: Optional[TracebackType], 

328 ) -> Optional[bool]: 

329 if not _exc_type: 

330 if self._ctx is not None: 

331 self._indent_compositor.__exit__(None, None, None) 

332 self._ctx.add_string(self.epilog) 

333 else: 

334 if self._ctx is not None: 

335 self._indent_compositor.__exit__(_exc_type, _exc_value, _traceback) 

336 return super()._exit(_exc_type, _exc_value, _traceback) 

337 

338 

339class Tagged(Logged): 

340 """The tagged compositor, which is used to wrap the content with a tag. 

341 

342 Note the indent will also applyt to the tag indicator. 

343 

344 Attributes: 

345 _indent_inside: 

346 The class default indentation inside prolog and epilog is 4 spaces. 

347 

348 Example: 

349 ```py 

350 >>> with Tagged("div"): 

351 ... "item1" 

352 ... "item2" 

353 <<< The prompt will be: 

354 <div> 

355 item1 

356 item2 

357 </div> 

358 ``` 

359 """ 

360 

361 _indent_inside: Optional[str] = "" 

362 

363 def __init__( 

364 self, 

365 tag: str, 

366 *args: Any, 

367 attrs: Optional[Dict[str, str]] = None, 

368 tag_begin: str = "<{}{}>", 

369 tag_end: str = "</{}>", 

370 indent_inside: Union[str, int, None] = None, 

371 **kwargs: Any, 

372 ) -> None: 

373 """Initialize the tagged compositor. 

374 

375 Args: 

376 tag: The tag name. 

377 *args: The arguments. 

378 attrs: The attributes of the tag. 

379 tag_begin: The format of tag begin string. 

380 tag_end: The format of tag end string. 

381 indent_inside: The indentation inside the tag. 

382 **kwargs: The keyword arguments. 

383 """ 

384 self._tag = tag 

385 self._attrs = attrs 

386 self._tag_begin = tag_begin 

387 self._tag_end = tag_end 

388 prolog = tag_begin.format(tag, self.formated_attrs) 

389 epilog = tag_end.format(tag) 

390 super().__init__( 

391 *args, prolog=prolog, epilog=epilog, indent_inside=indent_inside, **kwargs 

392 ) 

393 

394 @property 

395 def formated_attrs(self) -> str: 

396 """The formatted attributes of the tag.""" 

397 if self._attrs is None: 

398 return "" 

399 return " " + " ".join(f'{k}="{v}"' for k, v in self._attrs.items()) 

400 

401 

402class InlineTagged(Tagged): 

403 """The inline tagged compositor, which is used to wrap the content with a tag. 

404 

405 Attributes: 

406 _sep: The class default separator is "". 

407 _indexing: The class default indexing mode is no indexing. 

408 _new_indent: The class default indentation is "". 

409 _is_inline: The class default is True. 

410 _indent_inside: This class does not support indentation inside. 

411 

412 Example: 

413 ```py 

414 >>> with InlineTagged("div", sep=","): 

415 ... "item1" 

416 ... "item2" 

417 <<< The prompt will be: 

418 <div>item1,item2</div> 

419 ``` 

420 """ 

421 

422 def _get_kwargs_for_inner(self, kwargs: Dict[str, Any]) -> Dict[str, Any]: 

423 # pass the arguments to the inner compositor 

424 kwargs["sep"] = kwargs.get("sep", self._sep) 

425 kwargs["indexing"] = kwargs.get("indexing", self._indexing) 

426 kwargs["new_indent"] = self._new_indent 

427 kwargs["is_inline"] = self._is_inline 

428 return kwargs 

429 

430 _sep = "" 

431 _indexing = Indexing() 

432 _new_indent = "" 

433 _is_inline = True 

434 _indent_inside: Optional[str] = None 

435 

436 

437@need_ctx 

438def iter( 

439 lst: Iterable, 

440 comp: Optional[Compositor] = None, 

441 _ctx: Optional[PromptContext] = None, 

442) -> Iterable: 

443 """Iterate over the iterable list with the compositor. 

444 

445 Example: 

446 ```py 

447 >>> items = ["item1", "item2"] 

448 >>> for i in iter(items, NumberedList()): 

449 ... i 

450 <<< The prompt will be: 

451 1. item1 

452 2. item2 

453 ``` 

454 """ 

455 # support tqdm-like context manager 

456 if comp is None: 

457 comp = NumberedList(_ctx=_ctx) 

458 

459 entered = False 

460 try: 

461 for i in lst: 

462 if not entered: 

463 entered = True 

464 comp.__enter__() 

465 yield i 

466 except Exception as e: 

467 # TODO: check the impl here 

468 if entered: 

469 if not comp.__exit__(type(e), e, e.__traceback__): 

470 raise e 

471 else: 

472 raise e 

473 finally: 

474 if entered: 

475 comp.__exit__(None, None, None)