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

1from os import get_terminal_size 

2from time import sleep 

3from typing import Any 

4 

5import rich 

6from noiftimer import Timer 

7 

8 

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 

17 

18 

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`. 

27 

28 #### :params: 

29 

30 `animate`: Will cause `string` to be printed to the terminal one character at a time. 

31 

32 `animate_refresh`: Number of seconds between the addition of characters when `animate` is `True`. 

33 

34 `use_rich`: Use `rich` package to print `string`. 

35 

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 

56 

57 

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. 

60 

61 Similar visually to `print_in_place`, but for multiple lines. 

62 

63 #### *** Leaving this here for backwards compatibility, but just use `rich.Live` instead *** 

64 

65 #### :params: 

66 

67 `use_rich`: Use `rich` package to print `string`. 

68 

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 

83 

84 

85class ProgBar: 

86 """Self incrementing, dynamically sized progress bar. 

87 

88 Includes an internal timer that starts when this object is created. 

89 

90 Easily add runtime to progress display: 

91 

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%" """ 

96 

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: 

109 

110 `total`: The number of calls to reach 100% completion. 

111 

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() 

118 

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. 

121 

122 `fill_ch`: The character used to represent the completed part of the bar. 

123 

124 `unfill_ch`: The character used to represent the incomplete part of the bar. 

125 

126 `width_ratio`: The width of the progress bar relative to the width of the terminal window. 

127 

128 `new_line_after_completion`: Make a call to `print()` once `self.counter >= self.total`. 

129 

130 `clear_after_completion`: Make a call to `printbuddies.clear()` once `self.counter >= self.total`. 

131 

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 

143 

144 def __enter__(self): 

145 self.with_context = True 

146 return self 

147 

148 def __exit__(self, *args, **kwargs): 

149 if self.clear_after_completion: 

150 clear() 

151 else: 

152 print() 

153 

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() 

162 

163 @property 

164 def runtime(self) -> str: 

165 return f"runtime:{self.timer.elapsed_str}" 

166 

167 @property 

168 def bar(self) -> str: 

169 return f"{self.prefix}{' '*bool(self.prefix)}[{self.filled}{self.unfilled}]-{self.percent}% {self.suffix}" 

170 

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 

179 

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() 

187 

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 

194 

195 def get_bar(self): 

196 return f"{self.prefix}{' '*bool(self.prefix)}[{self.filled}{self.unfilled}]-{self.percent}% {self.suffix}" 

197 

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. 

207 

208 #### :params: 

209 

210 `prefix`: String affixed to the front of the progress bar. 

211 

212 `suffix`: String appended to the end of the progress bar. 

213 

214 `counter_override`: When an externally incremented completion counter is needed. 

215 

216 `total_override`: When an externally controlled bar total is needed. 

217 

218 `return_object`: An object to be returned by display(). 

219 Allows `display()` to be called within a comprehension: 

220 

221 e.g. 

222 

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 

261 

262 

263class Spinner: 

264 """Prints one of a sequence of characters in order everytime `display()` is called. 

265 

266 The `display` function writes the new character to the same line, overwriting the previous character. 

267 

268 The sequence will be cycled through indefinitely. 

269 

270 If used as a context manager, the last printed character will be cleared upon exiting. 

271 

272 #### *** Leaving this here for backwards compatibility, but just use `rich.console.Console().status()` instead *** 

273 """ 

274 

275 def __init__( 

276 self, sequence: list[str] = ["/", "-", "\\"], width_ratio: float = 0.25 

277 ): 

278 """ 

279 #### params: 

280 

281 `sequence`: Override the built in spin sequence. 

282 

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 

288 

289 def __enter__(self): 

290 return self 

291 

292 def __exit__(self, *args, **kwargs): 

293 clear() 

294 

295 @property 

296 def width_ratio(self) -> float: 

297 return self._width_ratio 

298 

299 @width_ratio.setter 

300 def width_ratio(self, ratio: float): 

301 self._width_ratio = ratio 

302 self._update_width() 

303 

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) 

307 

308 @property 

309 def sequence(self) -> list[Any]: 

310 return self._sequence 

311 

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] 

320 

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 

326 

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