Coverage for src/toolbox_python/output.py: 100%

37 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-13 07:24 +0000

1# ============================================================================ # 

2# # 

3# Title : Output # 

4# Purpose : Streamline how data is outputted. # 

5# Including `print`'ing and `logg`'ing # 

6# # 

7# ============================================================================ # 

8 

9 

10# ---------------------------------------------------------------------------- # 

11# # 

12# Overview #### 

13# # 

14# ---------------------------------------------------------------------------- # 

15 

16 

17# ---------------------------------------------------------------------------- # 

18# Description #### 

19# ---------------------------------------------------------------------------- # 

20 

21 

22""" 

23!!! note "Summary" 

24 The `output` module is for streamlining how data is outputted. 

25 This includes `#!py print()`'ing to the terminal and `#!py log()`'ing to files. 

26""" 

27 

28 

29# ---------------------------------------------------------------------------- # 

30# # 

31# Setup #### 

32# # 

33# ---------------------------------------------------------------------------- # 

34 

35 

36# ---------------------------------------------------------------------------- # 

37# Imports #### 

38# ---------------------------------------------------------------------------- # 

39 

40 

41# ## Python StdLib Imports ---- 

42from logging import Logger, _nameToLevel 

43from math import ceil 

44from typing import Any, Literal, Optional, Union 

45 

46# ## Python Third Party Imports ---- 

47from typeguard import typechecked 

48 

49# ## Local First Party Imports ---- 

50from toolbox_python.checkers import is_type 

51from toolbox_python.collection_types import log_levels, str_list 

52 

53 

54# ---------------------------------------------------------------------------- # 

55# Exports #### 

56# ---------------------------------------------------------------------------- # 

57 

58__all__: str_list = ["print_or_log_output", "list_columns"] 

59 

60 

61# ---------------------------------------------------------------------------- # 

62# # 

63# Functions #### 

64# # 

65# ---------------------------------------------------------------------------- # 

66 

67 

68@typechecked 

69def print_or_log_output( 

70 message: str, 

71 print_or_log: Literal["print", "log"] = "print", 

72 log: Optional[Logger] = None, 

73 log_level: Optional[log_levels] = None, 

74) -> None: 

75 """ 

76 !!! note "Summary" 

77 Determine whether to `#!py print()` or `#!py log()` a given `message`. 

78 

79 Params: 

80 message (str): 

81 The `message` to be processed. 

82 print_or_log (Optional[Literal["print", "log"]], optional): 

83 The option for what to do with the `message`.<br> 

84 Defaults to `#!py "print"`. 

85 log (Optional[Logger], optional): 

86 If `#!py print_or_log=="log"`, then this parameter must contain the `#!py Logger` object to be processed, 

87 otherwise it will raise an `#!py AssertError`.<br> 

88 Defaults to `#!py None`. 

89 log_level (Optional[_log_levels], optional): 

90 If `#!py print_or_log=="log"`, then this parameter must contain the required log level for the `message`. 

91 Must be one of the log-levels available in the `#!py logging` module.<br> 

92 Defaults to `#!py None`. 

93 

94 Raises: 

95 TypeError: 

96 If any of the inputs parsed to the parameters of this function are not the correct type. Uses the [`@typeguard.typechecked`](https://typeguard.readthedocs.io/en/stable/api.html#typeguard.typechecked) decorator. 

97 AssertError: 

98 If `#!py print_or_log=="log"` and `#!py log` is not an instance of `#!py Logger`. 

99 

100 Returns: 

101 (None): 

102 Nothing is returned. Only printed or logged. 

103 

104 ???+ example "Examples" 

105 

106 ```{.py .python linenums="1" title="Set up data for examples"} 

107 >>> from toolbox_python.output import print_or_log_output 

108 >>> import logging 

109 >>> logging.basicConfig(filename="logs.log", encoding="utf-8") 

110 >>> log = logging.getLogger("root") 

111 >>> default_message = "This is a" 

112 ``` 

113 

114 ```{.py .python linenums="1" title="Example 1: Print output"} 

115 >>> print_or_log_output( 

116 ... message=f"{default_message} print", 

117 ... print_or_log="print", 

118 ... ) 

119 ``` 

120 <div class="result" markdown> 

121 ```{.txt .text title="Terminal"} 

122 This is a print 

123 ``` 

124 !!! success "Conclusion: Successfully printed the message." 

125 </div> 

126 

127 ```{.py .python linenums="1" title="Example 2: Log `info`"} 

128 >>> print_or_log_output( 

129 ... message=f"{default_message}n info", 

130 ... print_or_log="log", 

131 ... log=log, 

132 ... log_level="info", 

133 ... ) 

134 ``` 

135 <div class="result" markdown> 

136 ```{.log .log title="logs.log"} 

137 INFO:root:This is an info 

138 ``` 

139 !!! success "Conclusion: Successfully logged the message." 

140 </div> 

141 

142 ```{.py .python linenums="1" title="Example 3: Log `debug`"} 

143 >>> print_or_log_output( 

144 ... message=f"{default_message} debug", 

145 ... print_or_log="log", 

146 ... log=log, 

147 ... log_level="debug", 

148 ... ) 

149 ``` 

150 <div class="result" markdown> 

151 ```{.log .log title="logs.log"} 

152 INFO:root:This is an info 

153 DEBUG:root:This is a debug 

154 ``` 

155 !!! success "Conclusion: Successfully added message to logs." 

156 !!! observation "Note: This logging structure will continue for every new call to `print_or_log_output()` when `print_or_log="log"`, and the `log` and `log_level` parameters are valid." 

157 </div> 

158 

159 ```{.py .python linenums="1" title="Example 7: Invalid `print_or_log` input"} 

160 >>> print_or_log_output( 

161 ... message=f"{default_message} invalid", 

162 ... print_or_log="error", 

163 ... ) 

164 ``` 

165 <div class="result" markdown> 

166 ```{.txt .text title="Terminal"} 

167 TypeError: ... 

168 ``` 

169 !!! failure "Conclusion: `print_or_log` can only have the string values `"print"` or `"log"`." 

170 </div> 

171 

172 ```{.py .python linenums="1" title="Example 8: Invalid `log` input"} 

173 >>> print_or_log_output( 

174 ... message=f"{default_message} invalid", 

175 ... print_or_log="log", 

176 ... log=None, 

177 ... log_level="info", 

178 ... ) 

179 ``` 

180 <div class="result" markdown> 

181 ```{.txt .text title="Terminal"} 

182 AssertionError: When `print_or_log=='log'` then `log` must be type `Logger`. Here, you have parsed: '<class 'NoneType'>' 

183 ``` 

184 !!! failure "Conclusion: When `print_or_log="log"` then `#!py log` must be an instance of `#!py Logger`." 

185 </div> 

186 

187 ```{.py .python linenums="1" title="Example 9: Invalid `log_level` input"} 

188 >>> print_or_log_output( 

189 ... message=f"{default_message} invalid", 

190 ... print_or_log="log", 

191 ... log=log, 

192 ... log_level="invalid", 

193 ... ) 

194 ``` 

195 <div class="result" markdown> 

196 ```{.txt .text title="Terminal"} 

197 TypeError: ... 

198 ``` 

199 !!! failure "Conclusion: `log_level` must be a valid log level from the `logging` module." 

200 </div> 

201 """ 

202 

203 # Early exit when printing the message 

204 if print_or_log == "print": 

205 print(message) 

206 return None 

207 

208 # Check in put for logging 

209 if not is_type(log, Logger): 

210 raise TypeError( 

211 f"When `print_or_log=='log'` then `log` must be type `Logger`. " 

212 f"Here, you have parsed: '{type(log)}'" 

213 ) 

214 if log_level is None: 

215 raise ValueError( 

216 f"When `print_or_log=='log'` then `log_level` must be parsed " 

217 f"with a valid value from: {log_levels}." 

218 ) 

219 

220 # Assertions to keep `mypy` happy 

221 assert log is not None 

222 assert log_level is not None 

223 

224 # Do logging 

225 log.log( 

226 level=_nameToLevel[log_level.upper()], 

227 msg=message, 

228 ) 

229 

230 

231@typechecked 

232def list_columns( 

233 obj: list, 

234 cols_wide: int = 4, 

235 columnwise: bool = True, 

236 gap: int = 4, 

237 print_output: bool = True, 

238) -> Optional[str]: 

239 """ 

240 !!! note Summary 

241 Print the given list in evenly-spaced columns. 

242 

243 Params: 

244 obj (list): 

245 The list to be formatted. 

246 

247 cols_wide (int, optional): 

248 The number of columns in which the list should be formatted.<br> 

249 Defaults to: `#!py 4`. 

250 

251 columnwise (bool, optional): 

252 Whether or not to print columnwise or rowwise. 

253 

254 - `#!py True`: Will be formatted column-wise. 

255 - `#!py False`: Will be formatted row-wise. 

256 

257 Defaults to: `#!py True`. 

258 

259 gap (int, optional): 

260 The number of spaces that should separate the longest column 

261 item/s from the next column. This is the effective spacing 

262 between columns based on the maximum `#!py len()` of the list items.<br> 

263 Defaults to: `#!py 4`. 

264 

265 print_output (bool, optional): 

266 Whether or not to print the output to the terminal. 

267 

268 - `#!py True`: Will print and return. 

269 - `#!py False`: Will not print; only return. 

270 

271 Defaults to: `#!py True`. 

272 

273 Raises: 

274 TypeError: 

275 If any of the inputs parsed to the parameters of this function are not the correct type. Uses the [`@typeguard.typechecked`](https://typeguard.readthedocs.io/en/stable/api.html#typeguard.typechecked) decorator. 

276 

277 Returns: 

278 printer (Optional[str]): 

279 The formatted string object. 

280 

281 ???+ example "Examples" 

282 

283 ```{.py .python linenums="1" title="Set up"} 

284 >>> # Imports 

285 >>> from toolbox_python.output import list_columns 

286 >>> import requests 

287 >>> 

288 >>> # Define function to fetch list of words 

289 >>> def get_list_of_words(num_words: int = 100): 

290 ... word_url = "https://www.mit.edu/~ecprice/wordlist.10000" 

291 ... response = requests.get(word_url) 

292 ... words = response.content.decode().splitlines() 

293 ... return words[:num_words] 

294 ``` 

295 

296 ```{.py .python linenums="1" title="Example 1: Default parameters"} 

297 >>> list_columns(get_list_of_words(4 * 5)) 

298 ``` 

299 <div class="result" markdown> 

300 ```{.txt .text title="Terminal"} 

301 a abandoned able abraham 

302 aa abc aboriginal abroad 

303 aaa aberdeen abortion abs 

304 aaron abilities about absence 

305 ab ability above absent 

306 ``` 

307 !!! success "Conclusion: Successfully printed the list in columns." 

308 </div> 

309 

310 ```{.py .python linenums="1" title="Example 2: Columnwise with 2 columns"} 

311 >>> list_columns( 

312 ... get_list_of_words(5), 

313 ... cols_wide=2, 

314 ... columnwise=True, 

315 ... ) 

316 ``` 

317 <div class="result" markdown> 

318 ```{.txt .text title="Terminal"} 

319 a aaron 

320 aa ab 

321 aaa 

322 ``` 

323 !!! success "Conclusion: Successfully printed the list in columns." 

324 </div> 

325 

326 ```{.py .python linenums="1" title="Example 3: Rowwise with 3 columns"} 

327 >>> list_columns( 

328 ... get_list_of_words(4 * 3), 

329 ... columnwise=False, 

330 ... cols_wide=3, 

331 ... print_output=True, 

332 ... ) 

333 ``` 

334 <div class="result" markdown> 

335 ```{.txt .text title="Terminal"} 

336 a aa aaa 

337 aaron ab abandoned 

338 abc aberdeen abilities 

339 ability able aboriginal 

340 ``` 

341 !!! success "Conclusion: Successfully printed the list in rows." 

342 </div> 

343 

344 ```{.py .python linenums="1" title="Example 4: Rowwise with 2 columns, no print output"} 

345 >>> output = list_columns( 

346 ... get_list_of_words(4 * 2), 

347 ... columnwise=False, 

348 ... cols_wide=2, 

349 ... print_output=False, 

350 ... ) 

351 >>> print(output) 

352 ``` 

353 <div class="result" markdown> 

354 ```{.txt .text title="Terminal"} 

355 a aa 

356 aaa aaron 

357 ab abandoned 

358 abc aberdeen 

359 ``` 

360 !!! success "Conclusion: Successfully returned the formatted string." 

361 </div> 

362 

363 ??? Success "Credit" 

364 Full credit goes to:<br> 

365 https://stackoverflow.com/questions/1524126/how-to-print-a-list-more-nicely#answer-36085705 

366 """ 

367 string_list: list[str] = [str(item) for item in obj] 

368 if cols_wide > len(string_list): 

369 cols_wide = len(string_list) 

370 max_len: int = max(len(item) for item in string_list) 

371 if columnwise: 

372 cols_wide = int(ceil(len(string_list) / cols_wide)) 

373 segmented_list: list[list[str]] = [ 

374 string_list[index : index + cols_wide] 

375 for index in range(0, len(string_list), cols_wide) 

376 ] 

377 if columnwise: 

378 if len(segmented_list[-1]) != cols_wide: 

379 segmented_list[-1].extend( 

380 [""] * (len(string_list) - len(segmented_list[-1])) 

381 ) 

382 combined_list: Union[list[list[str]], Any] = zip(*segmented_list) 

383 else: 

384 combined_list = segmented_list 

385 printer: str = "\n".join( 

386 [ 

387 "".join([element.ljust(max_len + gap) for element in group]) 

388 for group in combined_list 

389 ] 

390 ) 

391 if print_output: 

392 print(printer) 

393 return printer