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
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-22 15:39 -0800
1"""Containg the compositor classes.
3All examples shows the composed prompt in APPL functions.
4"""
6from __future__ import annotations
8from types import TracebackType
9from typing import Any, Dict, Iterable, Optional, Union
11from .const import INDENT4 as INDENT
12from .core import ApplStr, Compositor, Indexing, PromptContext
13from .func import need_ctx
16class LineSeparated(Compositor):
17 r"""The line separated compositor.
19 Attributes:
20 _sep: The class default separator is "\n".
22 Example:
23 ```py
24 >>> with LineSeparated():
25 ... "item1"
26 ... "item2"
27 <<< The prompt will be:
28 item1
29 item2
30 ```
31 """
33 _sep = "\n"
36class DoubleLineSeparated(Compositor):
37 r"""The double line separated compositor.
39 Attributes:
40 _sep: The class default separator is "\n\n".
42 Example:
43 ```py
44 >>> with DoubleLineSeparated():
45 ... "item1"
46 ... "item2"
47 <<< The prompt will be:
48 item1
50 item2
51 ```
52 """
54 _sep = "\n\n"
57class NoIndent(LineSeparated):
58 """The list compositor with no indentation.
60 Attributes:
61 _inc_indent: The class default indentation is "".
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 """
75 _new_indent = ""
78class IndentedList(LineSeparated):
79 """The indented list compositor.
81 Attributes:
82 _inc_indent: The class default indentation is INDENT.
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 """
97 _inc_indent = INDENT
100class NumberedList(LineSeparated):
101 """The number list compositor.
103 Attributes:
104 _indexing: The class default indexing mode is "number".
106 Example:
107 ```py
108 >>> with NumberedList():
109 ... "item1"
110 ... "item2"
111 <<< The prompt will be:
112 1. item1
113 2. item2
114 ```
115 """
117 _indexing = Indexing("number")
120class LowerLetterList(LineSeparated):
121 """The lower letter list compositor.
123 Attributes:
124 _indexing: The class default indexing mode is "lower".
126 Example:
127 ```py
128 >>> with LowerLetterList():
129 ... "item1"
130 ... "item2"
131 <<< The prompt will be:
132 a. item1
133 b. item2
134 ```
135 """
137 _indexing = Indexing("lower")
140class UpperLetterList(LineSeparated):
141 """The upper letter list compositor.
143 Attributes:
144 _indexing: The class default indexing mode is "upper".
146 Example:
147 ```py
148 >>> with UpperLetterList():
149 ... "item1"
150 ... "item2"
151 <<< The prompt will be:
152 A. item1
153 B. item2
154 ```
155 """
157 _indexing = Indexing("upper")
160class LowerRomanList(LineSeparated):
161 """The lower roman list compositor.
163 Attributes:
164 _indexing: The class default indexing mode is "roman".
166 Example:
167 ```py
168 >>> with LowerRomanList():
169 ... "item1"
170 ... "item2"
171 <<< The prompt will be:
172 i. item1
173 ii. item2
174 ```
175 """
177 _indexing = Indexing("roman")
180class UpperRomanList(LineSeparated):
181 """The upper roman list compositor.
183 Attributes:
184 _indexing: The class default indexing mode is "Roman".
186 Example:
187 ```py
188 >>> with UpperRomanList():
189 ... "item1"
190 ... "item2"
191 <<< The prompt will be:
192 I. item1
193 II. item2
194 ```
195 """
197 _indexing = Indexing("Roman")
200class DashList(LineSeparated):
201 """The dash list compositor.
203 Attributes:
204 _indexing: The class default indexing mode is "dash".
206 Example:
207 ```py
208 >>> with DashList():
209 ... "item1"
210 ... "item2"
211 <<< The prompt will be:
212 - item1
213 - item2
214 ```
215 """
217 _indexing = Indexing("dash")
220class StarList(LineSeparated):
221 """The star list compositor.
223 Attributes:
224 _indexing: The class default indexing mode is "star".
226 Example:
227 ```py
228 >>> with StarList():
229 ... "item1"
230 ... "item2"
231 <<< The prompt will be:
232 * item1
233 * item2
234 ```
235 """
237 _indexing = Indexing("star")
240LetterList = UpperLetterList
241"""The alias of UpperLetterList."""
242RomanList = UpperRomanList
243"""The alias of UpperRomanList."""
246class Logged(LineSeparated):
247 """The logged compositor, which is used to wrap the content with logs.
249 Note the indent will also apply to the prolog and epilog.
251 Attributes:
252 _indent_inside:
253 The class default indentation inside prolog and epilog is "".
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 """
268 _indent_inside: Optional[str] = ""
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.
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)
303 @property
304 def prolog(self) -> str:
305 """The prolog string."""
306 return self._prolog
308 @property
309 def epilog(self) -> str:
310 """The epilog string."""
311 return self._epilog
313 def _get_kwargs_for_inner(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
314 kwargs["indent"] = self._indent_inside
315 return kwargs
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__()
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)
339class Tagged(Logged):
340 """The tagged compositor, which is used to wrap the content with a tag.
342 Note the indent will also applyt to the tag indicator.
344 Attributes:
345 _indent_inside:
346 The class default indentation inside prolog and epilog is 4 spaces.
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 """
361 _indent_inside: Optional[str] = ""
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.
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 )
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())
402class InlineTagged(Tagged):
403 """The inline tagged compositor, which is used to wrap the content with a tag.
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.
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 """
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
430 _sep = ""
431 _indexing = Indexing()
432 _new_indent = ""
433 _is_inline = True
434 _indent_inside: Optional[str] = None
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.
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)
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)