Coverage for E:\1vsCode\python\printbuddies\src\printbuddies\printbubs.py: 22%
169 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-02-15 19:42 -0600
« prev ^ index » next coverage.py v7.2.2, created at 2024-02-15 19:42 -0600
1from os import get_terminal_size
2from time import sleep
3from typing import Any
5import rich
6from noiftimer import Timer
9def clear():
10 """Erase the current line from the terminal."""
11 try:
12 print(" " * (get_terminal_size().columns - 1), flush=True, end="\r")
13 except OSError:
14 ...
15 except Exception as e:
16 raise e
19def print_in_place(
20 string: str,
21 animate: bool = False,
22 animate_refresh: float = 0.01,
23 use_rich: bool = True,
24 truncate: bool = True,
25):
26 """Calls to `print_in_place` will overwrite the previous line of text in the terminal with `string`.
28 #### :params:
30 `animate`: Will cause `string` to be printed to the terminal one character at a time.
32 `animate_refresh`: Number of seconds between the addition of characters when `animate` is `True`.
34 `use_rich`: Use `rich` package to print `string`.
36 `truncate`: Truncate strings that are wider than the terminal window.
37 """
38 clear()
39 string = str(string)
40 if use_rich:
41 print = rich.print
42 try:
43 width = get_terminal_size().columns
44 if truncate:
45 string = string[: width - 2]
46 if animate:
47 for i in range(len(string)):
48 print(f"{string[:i+1]}", flush=True, end=" \r")
49 sleep(animate_refresh)
50 else:
51 print(string, flush=True, end="\r")
52 except OSError:
53 ...
54 except Exception as e:
55 raise e
58def ticker(info: list[str], use_rich: bool = True, truncate: bool = True):
59 """Prints `info` to terminal with top and bottom padding so that previous text is not visible.
61 Similar visually to `print_in_place`, but for multiple lines.
63 #### *** Leaving this here for backwards compatibility, but just use `rich.Live` instead ***
65 #### :params:
67 `use_rich`: Use `rich` package to print `string`.
69 `truncate`: Truncate strings that are wider than the terminal window."""
70 if use_rich:
71 print = rich.print
72 try:
73 width = get_terminal_size().columns
74 info = [str(line)[: width - 1] if truncate else str(line) for line in info]
75 height = get_terminal_size().lines - len(info)
76 print("\n" * (height * 2), end="")
77 print(*info, sep="\n", end="")
78 print("\n" * (int((height) / 2)), end="")
79 except OSError:
80 ...
81 except Exception as e:
82 raise e
85class ProgBar:
86 """Self incrementing, dynamically sized progress bar.
88 Includes an internal timer that starts when this object is created.
90 Easily add runtime to progress display:
92 >>> bar = ProgBar(total=100)
93 >>> time.sleep(30)
94 >>> bar.display(prefix=f"Doin stuff ~ {bar.runtime}")
95 >>> "Doin stuff ~ runtime: 30s [_///////////////////]-1.00%" """
97 def __init__(
98 self,
99 total: float,
100 update_frequency: int = 1,
101 fill_ch: str = "_",
102 unfill_ch: str = "/",
103 width_ratio: float = 0.5,
104 new_line_after_completion: bool = True,
105 clear_after_completion: bool = False,
106 ):
107 """
108 #### :params:
110 `total`: The number of calls to reach 100% completion.
112 `update_frequency`: The progress bar will only update once every this number of calls to `display()`.
113 The larger the value, the less performance impact `ProgBar` has on the loop in which it is called.
114 e.g.
115 >>> bar = ProgBar(100, update_frequency=10)
116 >>> for _ in range(100):
117 >>> bar.display()
119 ^The progress bar in the terminal will only update once every ten calls, going from 0%->100% in 10% increments.
120 Note: If `total` is not a multiple of `update_frequency`, the display will not show 100% completion when the loop finishes.
122 `fill_ch`: The character used to represent the completed part of the bar.
124 `unfill_ch`: The character used to represent the incomplete part of the bar.
126 `width_ratio`: The width of the progress bar relative to the width of the terminal window.
128 `new_line_after_completion`: Make a call to `print()` once `self.counter >= self.total`.
130 `clear_after_completion`: Make a call to `printbuddies.clear()` once `self.counter >= self.total`.
132 Note: if `new_line_after_completion` and `clear_after_completion` are both `True`, the line will be cleared
133 then a call to `print()` will be made."""
134 self.total = total
135 self.update_frequency = update_frequency
136 self.fill_ch = fill_ch[0]
137 self.unfill_ch = unfill_ch[0]
138 self.width_ratio = width_ratio
139 self.new_line_after_completion = new_line_after_completion
140 self.clear_after_completion = clear_after_completion
141 self.reset()
142 self.with_context = False
144 def __enter__(self):
145 self.with_context = True
146 return self
148 def __exit__(self, *args, **kwargs):
149 if self.clear_after_completion:
150 clear()
151 else:
152 print()
154 def reset(self):
155 self.counter = 1
156 self.percent = ""
157 self.prefix = ""
158 self.suffix = ""
159 self.filled = ""
160 self.unfilled = ""
161 self.timer = Timer(subsecond_resolution=False).start()
163 @property
164 def runtime(self) -> str:
165 return f"runtime:{self.timer.elapsed_str}"
167 @property
168 def bar(self) -> str:
169 return f"{self.prefix}{' '*bool(self.prefix)}[{self.filled}{self.unfilled}]-{self.percent}% {self.suffix}"
171 def get_percent(self) -> str:
172 """Returns the percentage completed to two decimal places as a string without the `%`."""
173 percent = str(round(100.0 * self.counter / self.total, 2))
174 if len(percent.split(".")[1]) == 1:
175 percent = percent + "0"
176 if len(percent.split(".")[0]) == 1:
177 percent = "0" + percent
178 return percent
180 def _prepare_bar(self):
181 self.terminal_width = get_terminal_size().columns - 1
182 bar_length = int(self.terminal_width * self.width_ratio)
183 progress = int(bar_length * min(self.counter / self.total, 1.0))
184 self.filled = self.fill_ch * progress
185 self.unfilled = self.unfill_ch * (bar_length - progress)
186 self.percent = self.get_percent()
188 def _trim_bar(self):
189 original_width = self.width_ratio
190 while len(self.bar) > self.terminal_width and self.width_ratio > 0:
191 self.width_ratio -= 0.01
192 self._prepare_bar()
193 self.width_ratio = original_width
195 def get_bar(self):
196 return f"{self.prefix}{' '*bool(self.prefix)}[{self.filled}{self.unfilled}]-{self.percent}% {self.suffix}"
198 def display(
199 self,
200 prefix: str = "",
201 suffix: str = "",
202 counter_override: float | None = None,
203 total_override: float | None = None,
204 return_object: Any | None = None,
205 ) -> Any:
206 """Writes the progress bar to the terminal.
208 #### :params:
210 `prefix`: String affixed to the front of the progress bar.
212 `suffix`: String appended to the end of the progress bar.
214 `counter_override`: When an externally incremented completion counter is needed.
216 `total_override`: When an externally controlled bar total is needed.
218 `return_object`: An object to be returned by display().
219 Allows `display()` to be called within a comprehension:
221 e.g.
223 >>> bar = ProgBar(10)
224 >>> def square(x: int | float)->int|float:
225 >>> return x * x
226 >>> myList = [bar.display(return_object=square(i)) for i in range(10)]
227 >>> <progress bar gets displayed>
228 >>> myList
229 >>> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]"""
230 if not self.timer.started:
231 self.timer.start()
232 if counter_override is not None:
233 self.counter = counter_override
234 if total_override:
235 self.total = total_override
236 # Don't wanna divide by 0 there, pal
237 while self.total <= 0:
238 self.total += 1
239 try:
240 if self.counter % self.update_frequency == 0:
241 self.prefix = prefix
242 self.suffix = suffix
243 self._prepare_bar()
244 self._trim_bar()
245 pad = " " * (self.terminal_width - len(self.bar))
246 width = get_terminal_size().columns
247 print(f"{self.bar}{pad}"[: width - 2], flush=True, end="\r")
248 if self.counter >= self.total:
249 self.timer.stop()
250 if not self.with_context:
251 if self.clear_after_completion:
252 clear()
253 if self.new_line_after_completion:
254 print()
255 self.counter += 1
256 except OSError:
257 ...
258 except Exception as e:
259 raise e
260 return return_object
263class Spinner:
264 """Prints one of a sequence of characters in order everytime `display()` is called.
266 The `display` function writes the new character to the same line, overwriting the previous character.
268 The sequence will be cycled through indefinitely.
270 If used as a context manager, the last printed character will be cleared upon exiting.
272 #### *** Leaving this here for backwards compatibility, but just use `rich.console.Console().status()` instead ***
273 """
275 def __init__(
276 self, sequence: list[str] = ["/", "-", "\\"], width_ratio: float = 0.25
277 ):
278 """
279 #### params:
281 `sequence`: Override the built in spin sequence.
283 `width_ratio`: The fractional amount of the terminal for characters to move across.
284 """
285 self._base_sequence = sequence
286 self.width_ratio = width_ratio
287 self.sequence = self._base_sequence
289 def __enter__(self):
290 return self
292 def __exit__(self, *args, **kwargs):
293 clear()
295 @property
296 def width_ratio(self) -> float:
297 return self._width_ratio
299 @width_ratio.setter
300 def width_ratio(self, ratio: float):
301 self._width_ratio = ratio
302 self._update_width()
304 def _update_width(self):
305 self._current_terminal_width = get_terminal_size().columns
306 self._width = int((self._current_terminal_width - 1) * self.width_ratio)
308 @property
309 def sequence(self) -> list[Any]:
310 return self._sequence
312 @sequence.setter
313 def sequence(self, character_list: list[Any]):
314 self._sequence = [
315 ch.rjust(i + j)
316 for i in range(1, self._width, len(character_list))
317 for j, ch in enumerate(character_list)
318 ]
319 self._sequence += self._sequence[::-1]
321 def _get_next(self) -> str:
322 """Pop the first element of `self._sequence`, append it to the end, and return the element."""
323 ch = self.sequence.pop(0)
324 self.sequence.append(ch)
325 return ch
327 def display(self):
328 """Print the next character in the sequence."""
329 try:
330 if get_terminal_size().columns != self._current_terminal_width:
331 self._update_width()
332 self.sequence = self._base_sequence
333 print_in_place(self._get_next())
334 except OSError:
335 ...
336 except Exception as e:
337 raise e