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

38 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-24 10:34 +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 collections.abc import Generator 

43from logging import Logger, _nameToLevel 

44from math import ceil 

45from typing import Any, Literal, Optional, Union 

46 

47# ## Python Third Party Imports ---- 

48from typeguard import typechecked 

49 

50# ## Local First Party Imports ---- 

51from toolbox_python.checkers import is_type 

52from toolbox_python.collection_types import ( 

53 any_list, 

54 any_set, 

55 any_tuple, 

56 log_levels, 

57 str_list, 

58) 

59 

60 

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

62# Exports #### 

63# ---------------------------------------------------------------------------- # 

64 

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

66 

67 

68# ---------------------------------------------------------------------------- # 

69# # 

70# Functions #### 

71# # 

72# ---------------------------------------------------------------------------- # 

73 

74 

75@typechecked 

76def print_or_log_output( 

77 message: str, 

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

79 log: Optional[Logger] = None, 

80 log_level: Optional[log_levels] = None, 

81) -> None: 

82 """ 

83 !!! note "Summary" 

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

85 

86 Params: 

87 message (str): 

88 The `message` to be processed. 

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

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

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

92 log (Optional[Logger], optional): 

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

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

95 Defaults to `#!py None`. 

96 log_level (Optional[_log_levels], optional): 

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

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

99 Defaults to `#!py None`. 

100 

101 Raises: 

102 TypeError: 

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

104 AssertError: 

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

106 

107 Returns: 

108 (None): 

109 Nothing is returned. Only printed or logged. 

110 

111 ???+ example "Examples" 

112 

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

114 >>> from toolbox_python.output import print_or_log_output 

115 >>> import logging 

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

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

118 >>> default_message = "This is a" 

119 ``` 

120 

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

122 >>> print_or_log_output( 

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

124 ... print_or_log="print", 

125 ... ) 

126 ``` 

127 <div class="result" markdown> 

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

129 This is a print 

130 ``` 

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

132 </div> 

133 

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

135 >>> print_or_log_output( 

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

137 ... print_or_log="log", 

138 ... log=log, 

139 ... log_level="info", 

140 ... ) 

141 ``` 

142 <div class="result" markdown> 

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

144 INFO:root:This is an info 

145 ``` 

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

147 </div> 

148 

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

150 >>> print_or_log_output( 

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

152 ... print_or_log="log", 

153 ... log=log, 

154 ... log_level="debug", 

155 ... ) 

156 ``` 

157 <div class="result" markdown> 

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

159 INFO:root:This is an info 

160 DEBUG:root:This is a debug 

161 ``` 

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

163 !!! 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." 

164 </div> 

165 

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

167 >>> print_or_log_output( 

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

169 ... print_or_log="error", 

170 ... ) 

171 ``` 

172 <div class="result" markdown> 

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

174 TypeError: ... 

175 ``` 

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

177 </div> 

178 

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

180 >>> print_or_log_output( 

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

182 ... print_or_log="log", 

183 ... log=None, 

184 ... log_level="info", 

185 ... ) 

186 ``` 

187 <div class="result" markdown> 

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

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

190 ``` 

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

192 </div> 

193 

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

195 >>> print_or_log_output( 

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

197 ... print_or_log="log", 

198 ... log=log, 

199 ... log_level="invalid", 

200 ... ) 

201 ``` 

202 <div class="result" markdown> 

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

204 TypeError: ... 

205 ``` 

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

207 </div> 

208 """ 

209 

210 # Early exit when printing the message 

211 if print_or_log == "print": 

212 print(message) 

213 return None 

214 

215 # Check in put for logging 

216 if not is_type(log, Logger): 

217 raise TypeError( 

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

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

220 ) 

221 if log_level is None: 

222 raise ValueError( 

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

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

225 ) 

226 

227 # Assertions to keep `mypy` happy 

228 assert log is not None 

229 assert log_level is not None 

230 

231 # Do logging 

232 log.log( 

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

234 msg=message, 

235 ) 

236 

237 

238@typechecked 

239def list_columns( 

240 obj: Union[any_list, any_set, any_tuple, Generator], 

241 cols_wide: int = 4, 

242 columnwise: bool = True, 

243 gap: int = 4, 

244 print_output: bool = False, 

245) -> Optional[str]: 

246 """ 

247 !!! note Summary 

248 Print the given list in evenly-spaced columns. 

249 

250 Params: 

251 obj (list): 

252 The list to be formatted. 

253 

254 cols_wide (int, optional): 

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

256 Defaults to: `#!py 4`. 

257 

258 columnwise (bool, optional): 

259 Whether or not to print columnwise or rowwise. 

260 

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

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

263 

264 Defaults to: `#!py True`. 

265 

266 gap (int, optional): 

267 The number of spaces that should separate the longest column 

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

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

270 Defaults to: `#!py 4`. 

271 

272 print_output (bool, optional): 

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

274 

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

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

277 

278 Defaults to: `#!py True`. 

279 

280 Raises: 

281 TypeError: 

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

283 

284 Returns: 

285 printer (Optional[str]): 

286 The formatted string object. 

287 

288 ???+ example "Examples" 

289 

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

291 >>> # Imports 

292 >>> from toolbox_python.output import list_columns 

293 >>> import requests 

294 >>> 

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

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

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

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

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

300 ... return words[:num_words] 

301 ``` 

302 

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

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

305 ``` 

306 <div class="result" markdown> 

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

308 a abandoned able abraham 

309 aa abc aboriginal abroad 

310 aaa aberdeen abortion abs 

311 aaron abilities about absence 

312 ab ability above absent 

313 ``` 

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

315 </div> 

316 

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

318 >>> list_columns( 

319 ... get_list_of_words(5), 

320 ... cols_wide=2, 

321 ... columnwise=True, 

322 ... ) 

323 ``` 

324 <div class="result" markdown> 

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

326 a aaron 

327 aa ab 

328 aaa 

329 ``` 

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

331 </div> 

332 

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

334 >>> list_columns( 

335 ... get_list_of_words(4 * 3), 

336 ... columnwise=False, 

337 ... cols_wide=3, 

338 ... print_output=True, 

339 ... ) 

340 ``` 

341 <div class="result" markdown> 

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

343 a aa aaa 

344 aaron ab abandoned 

345 abc aberdeen abilities 

346 ability able aboriginal 

347 ``` 

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

349 </div> 

350 

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

352 >>> output = list_columns( 

353 ... get_list_of_words(4 * 2), 

354 ... columnwise=False, 

355 ... cols_wide=2, 

356 ... print_output=False, 

357 ... ) 

358 >>> print(output) 

359 ``` 

360 <div class="result" markdown> 

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

362 a aa 

363 aaa aaron 

364 ab abandoned 

365 abc aberdeen 

366 ``` 

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

368 </div> 

369 

370 ??? Success "Credit" 

371 Full credit goes to:<br> 

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

373 """ 

374 string_list: str_list = [str(item) for item in obj] 

375 if cols_wide > len(string_list): 

376 cols_wide = len(string_list) 

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

378 if columnwise: 

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

380 segmented_list: list[str_list] = [ 

381 string_list[index : index + cols_wide] 

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

383 ] 

384 if columnwise: 

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

386 segmented_list[-1].extend( 

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

388 ) 

389 combined_list: Union[list[str_list], Any] = zip(*segmented_list) 

390 else: 

391 combined_list = segmented_list 

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

393 [ 

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

395 for group in combined_list 

396 ] 

397 ) 

398 if print_output: 

399 print(printer) 

400 return printer