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
« 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# ============================================================================ #
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 collections.abc import Generator
43from logging import Logger, _nameToLevel
44from math import ceil
45from typing import Any, Literal, Optional, Union
47# ## Python Third Party Imports ----
48from typeguard import typechecked
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)
61# ---------------------------------------------------------------------------- #
62# Exports ####
63# ---------------------------------------------------------------------------- #
65__all__: str_list = ["print_or_log_output", "list_columns"]
68# ---------------------------------------------------------------------------- #
69# #
70# Functions ####
71# #
72# ---------------------------------------------------------------------------- #
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`.
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`.
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`.
107 Returns:
108 (None):
109 Nothing is returned. Only printed or logged.
111 ???+ example "Examples"
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 ```
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>
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>
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>
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>
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>
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 """
210 # Early exit when printing the message
211 if print_or_log == "print":
212 print(message)
213 return None
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 )
227 # Assertions to keep `mypy` happy
228 assert log is not None
229 assert log_level is not None
231 # Do logging
232 log.log(
233 level=_nameToLevel[log_level.upper()],
234 msg=message,
235 )
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.
250 Params:
251 obj (list):
252 The list to be formatted.
254 cols_wide (int, optional):
255 The number of columns in which the list should be formatted.<br>
256 Defaults to: `#!py 4`.
258 columnwise (bool, optional):
259 Whether or not to print columnwise or rowwise.
261 - `#!py True`: Will be formatted column-wise.
262 - `#!py False`: Will be formatted row-wise.
264 Defaults to: `#!py True`.
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`.
272 print_output (bool, optional):
273 Whether or not to print the output to the terminal.
275 - `#!py True`: Will print and return.
276 - `#!py False`: Will not print; only return.
278 Defaults to: `#!py True`.
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.
284 Returns:
285 printer (Optional[str]):
286 The formatted string object.
288 ???+ example "Examples"
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 ```
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>
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>
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>
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>
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