Coverage for src / toolbox_python / output.py: 100%
45 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-02 22:56 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-02 22:56 +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 Collection, Generator
43from logging import Logger, _nameToLevel
44from math import ceil
45from typing import Any, Literal, Optional, Union, overload
47# ## Python Third Party Imports ----
48from typeguard import typechecked
50# ## Local First Party Imports ----
51from toolbox_python.checkers import (
52 assert_all_is_type,
53 assert_is_type,
54 assert_is_valid,
55 is_type,
56)
59# ---------------------------------------------------------------------------- #
60# Exports ####
61# ---------------------------------------------------------------------------- #
64__all__: list[str] = ["print_or_log_output", "list_columns", "log_levels"]
67## --------------------------------------------------------------------------- #
68## Constants ####
69## --------------------------------------------------------------------------- #
72log_levels = Literal["debug", "info", "warning", "error", "critical"]
73"""
74!!! note "Summary"
75 To streamline other functions, this `type` alias is created for all of the `log` levels available.
76!!! abstract "Details"
77 The structure of the `type` is as follows:
78 ```pycon {.py .python linenums="1" title="Type structure"}
79 Literal["debug", "info", "warning", "error", "critical"]
80 ```
81"""
84# ---------------------------------------------------------------------------- #
85# #
86# Functions ####
87# #
88# ---------------------------------------------------------------------------- #
91@overload
92def print_or_log_output(
93 message: str,
94 print_or_log: Literal["print"],
95) -> None: ...
96@overload
97def print_or_log_output(
98 message: str,
99 print_or_log: Literal["log"],
100 *,
101 log: Logger,
102 log_level: log_levels = "info",
103) -> None: ...
104@overload
105def print_or_log_output(
106 message: str,
107 print_or_log: Optional[Literal["print", "log"]] = None,
108 *,
109 log: Optional[Logger] = None,
110 log_level: Optional[log_levels] = None,
111) -> None: ...
112@typechecked
113def print_or_log_output(
114 message: str,
115 print_or_log: Optional[Literal["print", "log"]] = "print",
116 *,
117 log: Optional[Logger] = None,
118 log_level: Optional[log_levels] = None,
119) -> None:
120 """
121 !!! note "Summary"
122 Determine whether to `#!py print()` or `#!py log()` a given `message`.
124 Params:
125 message (str):
126 The `message` to be processed.
127 print_or_log (Optional[Literal["print", "log"]], optional):
128 The option for what to do with the `message`.<br>
129 Defaults to `#!py "print"`.
130 log (Optional[Logger], optional):
131 If `#!py print_or_log=="log"`, then this parameter must contain the `#!py Logger` object to be processed,
132 otherwise it will raise an `#!py AssertError`.<br>
133 Defaults to `#!py None`.
134 log_level (Optional[log_levels], optional):
135 If `#!py print_or_log=="log"`, then this parameter must contain the required log level for the `message`.
136 Must be one of the log-levels available in the `#!py logging` module.<br>
137 Defaults to `#!py None`.
139 Raises:
140 (TypeCheckError):
141 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.
142 (AssertError):
143 If `#!py print_or_log=="log"` and `#!py log` is not an instance of `#!py Logger`.
145 Returns:
146 (None):
147 Nothing is returned. Only printed or logged.
149 ???+ example "Examples"
151 ```pycon {.py .python linenums="1" title="Set up data for examples"}
152 >>> from toolbox_python.output import print_or_log_output
153 >>> import logging
154 >>> logging.basicConfig(filename="logs.log", encoding="utf-8")
155 >>> log = logging.getLogger("root")
156 >>> default_message = "This is a"
157 ```
159 ```pycon {.py .python linenums="1" title="Example 1: Print output"}
160 >>> print_or_log_output(
161 ... message=f"{default_message} print",
162 ... print_or_log="print",
163 ... )
164 ```
165 <div class="result" markdown>
166 ```{.txt .text title="Terminal"}
167 This is a print
168 ```
169 !!! success "Conclusion: Successfully printed the message."
170 </div>
172 ```pycon {.py .python linenums="1" title="Example 2: Log `info`"}
173 >>> print_or_log_output(
174 ... message=f"{default_message}n info",
175 ... print_or_log="log",
176 ... log=log,
177 ... log_level="info",
178 ... )
179 ```
180 <div class="result" markdown>
181 ```{.log .log title="logs.log"}
182 INFO:root:This is an info
183 ```
184 !!! success "Conclusion: Successfully logged the message."
185 </div>
187 ```pycon {.py .python linenums="1" title="Example 3: Log `debug`"}
188 >>> print_or_log_output(
189 ... message=f"{default_message} debug",
190 ... print_or_log="log",
191 ... log=log,
192 ... log_level="debug",
193 ... )
194 ```
195 <div class="result" markdown>
196 ```{.log .log title="logs.log"}
197 INFO:root:This is an info
198 DEBUG:root:This is a debug
199 ```
200 !!! success "Conclusion: Successfully added message to logs."
201 !!! 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."
202 </div>
204 ```pycon {.py .python linenums="1" title="Example 7: Invalid `print_or_log` input"}
205 >>> print_or_log_output(
206 ... message=f"{default_message} invalid",
207 ... print_or_log="error",
208 ... )
209 ```
210 <div class="result" markdown>
211 ```{.txt .text title="Terminal"}
212 TypeError: ...
213 ```
214 !!! failure "Conclusion: `print_or_log` can only have the string values `"print"` or `"log"`."
215 </div>
217 ```pycon {.py .python linenums="1" title="Example 8: Invalid `log` input"}
218 >>> print_or_log_output(
219 ... message=f"{default_message} invalid",
220 ... print_or_log="log",
221 ... log=None,
222 ... log_level="info",
223 ... )
224 ```
225 <div class="result" markdown>
226 ```{.txt .text title="Terminal"}
227 AssertionError: When `print_or_log=='log'` then `log` must be type `Logger`. Here, you have parsed: '<class 'NoneType'>'
228 ```
229 !!! failure "Conclusion: When `print_or_log="log"` then `#!py log` must be an instance of `#!py Logger`."
230 </div>
232 ```pycon {.py .python linenums="1" title="Example 9: Invalid `log_level` input"}
233 >>> print_or_log_output(
234 ... message=f"{default_message} invalid",
235 ... print_or_log="log",
236 ... log=log,
237 ... log_level="invalid",
238 ... )
239 ```
240 <div class="result" markdown>
241 ```{.txt .text title="Terminal"}
242 TypeError: ...
243 ```
244 !!! failure "Conclusion: `log_level` must be a valid log level from the `logging` module."
245 </div>
246 """
248 # Early exit when printing the message
249 if print_or_log == "print":
250 print(message)
251 return None
253 # Check in put for logging
254 if not is_type(log, Logger):
255 raise TypeError(
256 f"When `print_or_log=='log'` then `log` must be type `Logger`. " f"Here, you have parsed: '{type(log)}'"
257 )
258 if log_level is None:
259 raise ValueError(
260 f"When `print_or_log=='log'` then `log_level` must be parsed " f"with a valid value from: {log_levels}."
261 )
263 # Assertions to keep `mypy` happy
264 assert print_or_log is not None
265 assert log is not None
266 assert log_level is not None
268 # Do logging
269 log.log(
270 level=_nameToLevel[log_level.upper()],
271 msg=message,
272 )
274 # Return
275 return None
278@typechecked
279def list_columns(
280 obj: Union[Collection[Any], Generator],
281 cols_wide: int = 4,
282 columnwise: bool = True,
283 gap: int = 4,
284 print_output: Literal[True, False] = False,
285) -> Optional[str]:
286 """
287 !!! note "Summary"
288 Print the given list in evenly-spaced columns.
290 Params:
291 obj (Union[Collection[Any], Generator]):
292 The list to be formatted.
294 cols_wide (int, optional):
295 The number of columns in which the list should be formatted.<br>
296 Defaults to: `#!py 4`.
298 columnwise (bool, optional):
299 Whether or not to print columnwise or rowwise.
301 - `#!py True`: Will be formatted column-wise.
302 - `#!py False`: Will be formatted row-wise.
304 Defaults to: `#!py True`.
306 gap (int, optional):
307 The number of spaces that should separate the longest column
308 item/s from the next column. This is the effective spacing
309 between columns based on the maximum `#!py len()` of the list items.<br>
310 Defaults to: `#!py 4`.
312 print_output (Literal[True, False], optional):
313 Whether or not to print the output to the terminal.
315 - `#!py True`: Will print and return.
316 - `#!py False`: Will not print; only return.
318 Defaults to: `#!py True`.
320 Raises:
321 (TypeCheckError):
322 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.
323 (TypeError):
324 If `#!py obj` is not a valid type. Must be one of: `#!py list`, `#!py set`, `#!py tuple`, or `#!py Generator`.
325 (ValueError):
326 If `#!py cols_wide` is not greater than `0`, or if `#!py gap` is not greater than `0`.
328 Returns:
329 printer (Optional[str]):
330 The formatted string object.
332 ???+ example "Examples"
334 ```pycon {.py .python linenums="1" title="Set up"}
335 >>> # Imports
336 >>> from toolbox_python.output import list_columns
337 >>> import requests
338 >>>
339 >>> # Define function to fetch list of words
340 >>> def get_list_of_words(num_words: int = 100):
341 ... word_url = "https://www.mit.edu/~ecprice/wordlist.10000"
342 ... response = requests.get(word_url)
343 ... words = response.content.decode().splitlines()
344 ... return words[:num_words]
345 ...
346 ```
348 ```pycon {.py .python linenums="1" title="Example 1: Default parameters"}
349 >>> list_columns(get_list_of_words(4 * 5))
350 ```
351 <div class="result" markdown>
352 ```{.txt .text title="Terminal"}
353 a abandoned able abraham
354 aa abc aboriginal abroad
355 aaa aberdeen abortion abs
356 aaron abilities about absence
357 ab ability above absent
358 ```
359 !!! success "Conclusion: Successfully printed the list in columns."
360 </div>
362 ```pycon {.py .python linenums="1" title="Example 2: Columnwise with 2 columns"}
363 >>> list_columns(
364 ... get_list_of_words(5),
365 ... cols_wide=2,
366 ... columnwise=True,
367 ... )
368 ```
369 <div class="result" markdown>
370 ```{.txt .text title="Terminal"}
371 a aaron
372 aa ab
373 aaa
374 ```
375 !!! success "Conclusion: Successfully printed the list in columns."
376 </div>
378 ```pycon {.py .python linenums="1" title="Example 3: Rowwise with 3 columns"}
379 >>> list_columns(
380 ... get_list_of_words(4 * 3),
381 ... columnwise=False,
382 ... cols_wide=3,
383 ... print_output=True,
384 ... )
385 ```
386 <div class="result" markdown>
387 ```{.txt .text title="Terminal"}
388 a aa aaa
389 aaron ab abandoned
390 abc aberdeen abilities
391 ability able aboriginal
392 ```
393 !!! success "Conclusion: Successfully printed the list in rows."
394 </div>
396 ```pycon {.py .python linenums="1" title="Example 4: Rowwise with 2 columns, no print output"}
397 >>> output = list_columns(
398 ... get_list_of_words(4 * 2),
399 ... columnwise=False,
400 ... cols_wide=2,
401 ... print_output=False,
402 ... )
403 >>> print(output)
404 ```
405 <div class="result" markdown>
406 ```{.txt .text title="Terminal"}
407 a aa
408 aaa aaron
409 ab abandoned
410 abc aberdeen
411 ```
412 !!! success "Conclusion: Successfully returned the formatted string."
413 </div>
415 ??? Success "Credit"
416 Full credit goes to:<br>
417 https://stackoverflow.com/questions/1524126/how-to-print-a-list-more-nicely#answer-36085705
418 """
420 # Validations
421 assert_is_type(obj, (list, set, tuple, Generator))
422 assert_all_is_type((cols_wide, gap), int)
423 assert_all_is_type((columnwise, print_output), bool)
424 assert_is_valid(cols_wide, ">", 0)
425 assert_is_valid(gap, ">", 0)
427 # Prepare the string representation of the object
428 string_list: list[str] = [str(item) for item in obj]
429 cols_wide = min(cols_wide, len(string_list))
430 max_len: int = max(len(item) for item in string_list)
432 # Adjust column width if column-wise output
433 if columnwise:
434 cols_wide = int(ceil(len(string_list) / cols_wide))
436 # Segment the list into chunks
437 segmented_list: list[list[str]] = [
438 string_list[index : index + cols_wide] for index in range(0, len(string_list), cols_wide)
439 ]
441 # Ensure the last segment has the correct number of columns
442 if columnwise:
443 if len(segmented_list[-1]) != cols_wide:
444 segmented_list[-1].extend([""] * (len(string_list) - len(segmented_list[-1])))
445 combined_list: Union[list[list[str]], Any] = zip(*segmented_list)
446 else:
447 combined_list = segmented_list
449 # Create the formatted string with proper spacing
450 printer: str = "\n".join(["".join([element.ljust(max_len + gap) for element in group]) for group in combined_list])
452 # Print the output if requested
453 if print_output:
454 print(printer)
455 return printer