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
« 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# ============================================================================ #
10# ---------------------------------------------------------------------------- #
11# #
12# Overview ####
13# #
14# ---------------------------------------------------------------------------- #
17# ---------------------------------------------------------------------------- #
18# Description ####
19# ---------------------------------------------------------------------------- #
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"""
29# ---------------------------------------------------------------------------- #
30# #
31# Setup ####
32# #
33# ---------------------------------------------------------------------------- #
36# ---------------------------------------------------------------------------- #
37# Imports ####
38# ---------------------------------------------------------------------------- #
41# ## Python StdLib Imports ----
42from logging import Logger, _nameToLevel
43from math import ceil
44from typing import Any, Literal, Optional, Union
46# ## Python Third Party Imports ----
47from typeguard import typechecked
49# ## Local First Party Imports ----
50from toolbox_python.checkers import is_type
51from toolbox_python.collection_types import log_levels, str_list
54# ---------------------------------------------------------------------------- #
55# Exports ####
56# ---------------------------------------------------------------------------- #
58__all__: str_list = ["print_or_log_output", "list_columns"]
61# ---------------------------------------------------------------------------- #
62# #
63# Functions ####
64# #
65# ---------------------------------------------------------------------------- #
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`.
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`.
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`.
100 Returns:
101 (None):
102 Nothing is returned. Only printed or logged.
104 ???+ example "Examples"
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 ```
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>
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>
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>
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>
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>
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 """
203 # Early exit when printing the message
204 if print_or_log == "print":
205 print(message)
206 return None
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 )
220 # Assertions to keep `mypy` happy
221 assert log is not None
222 assert log_level is not None
224 # Do logging
225 log.log(
226 level=_nameToLevel[log_level.upper()],
227 msg=message,
228 )
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.
243 Params:
244 obj (list):
245 The list to be formatted.
247 cols_wide (int, optional):
248 The number of columns in which the list should be formatted.<br>
249 Defaults to: `#!py 4`.
251 columnwise (bool, optional):
252 Whether or not to print columnwise or rowwise.
254 - `#!py True`: Will be formatted column-wise.
255 - `#!py False`: Will be formatted row-wise.
257 Defaults to: `#!py True`.
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`.
265 print_output (bool, optional):
266 Whether or not to print the output to the terminal.
268 - `#!py True`: Will print and return.
269 - `#!py False`: Will not print; only return.
271 Defaults to: `#!py True`.
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.
277 Returns:
278 printer (Optional[str]):
279 The formatted string object.
281 ???+ example "Examples"
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 ```
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>
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>
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>
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>
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