Coverage for src / toolbox_python / defaults.py: 100%
40 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 : Defaults #
4# Purpose : Enable setting and utilisation of default values. #
5# #
6# ============================================================================ #
9# ---------------------------------------------------------------------------- #
10# #
11# Overview ####
12# #
13# ---------------------------------------------------------------------------- #
16# ---------------------------------------------------------------------------- #
17# Description ####
18# ---------------------------------------------------------------------------- #
21"""
22!!! note "Summary"
23 The `defaults` module is used how to set and control default values for our various Python processes.
24"""
27# ---------------------------------------------------------------------------- #
28# #
29# Setup ####
30# #
31# ---------------------------------------------------------------------------- #
34# ---------------------------------------------------------------------------- #
35# Imports ####
36# ---------------------------------------------------------------------------- #
39# ## Future Python Library Imports ----
40from __future__ import annotations
42# ## Python StdLib Imports ----
43from typing import Any, Optional, Union
45# ## Python Third Party Imports ----
46from typeguard import typechecked
48# ## Local First Party Imports ----
49from toolbox_python.bools import strtobool
50from toolbox_python.checkers import is_type
53# ---------------------------------------------------------------------------- #
54# Exports ####
55# ---------------------------------------------------------------------------- #
58__all__: list[str] = ["defaults", "Defaults"]
61# ---------------------------------------------------------------------------- #
62# #
63# Classes ####
64# #
65# ---------------------------------------------------------------------------- #
68# ---------------------------------------------------------------------------- #
69# Defaults Class ####
70# ---------------------------------------------------------------------------- #
73class Defaults:
74 """
75 !!! note "Summary"
76 When we create and use Python variables, it is sometimes handy to add a default value for that variable.
77 This class will handle that process.
79 Methods:
80 - get(): From the value that is parsed in to the `value` parameter, convert it to `default` if `value` is `#!py None`, and convert it to `cast` if `cast` is not `#!py None`.
81 - _validate_value_and_default(): Validate to ensure that `value` and `default` are not both `#!py None`.
82 - _validate_type(): Check to ensure that `check_type` is a valid Python type
84 ???+ example "Examples"
86 ```pycon {.py .python linenums="1" title="Set up data for examples"}
87 >>> from toolbox_python.defaults import Defaults
88 >>> defaults = Defaults()
89 ```
91 ```pycon {.py .python linenums="1" title="Example 1: Call direct from class"}
92 >>> print(Defaults()(value="this"))
93 ```
94 <div class="result" markdown>
95 ```{.txt .text title="Terminal"}
96 "this"
97 ```
98 !!! success "Conclusion: Successfully printed default value direct from class."
99 </div>
101 ```pycon {.py .python linenums="1" title="Example 2: Call from instantiated class"}
102 >>> print(defaults(value="that"))
103 ```
104 <div class="result" markdown>
105 ```{.txt .text title="Terminal"}
106 "that"
107 ```
108 !!! success "Conclusion: Successfully printed default value from instantiated class."
109 </div>
111 ```pycon {.py .python linenums="1" title="Example 3: Cast to `bool`"}
112 >>> print(defaults(value="True", cast=bool))
113 ```
114 <div class="result" markdown>
115 ```{.txt .text title="Terminal"}
116 True
117 ```
118 !!! success "Conclusion: Successfully casted to `#!py bool`."
119 </div>
121 ```pycon {.py .python linenums="1" title="Example 4: Cast to `int`"}
122 >>> print(defaults(value="1", cast=int))
123 ```
124 <div class="result" markdown>
125 ```{.txt .text title="Terminal"}
126 1
127 ```
128 !!! success "Conclusion: Successfully casted to `#!py int`."
129 </div>
131 ```pycon {.py .python linenums="1" title="Example 5: Cast to `str`"}
132 >>> print(defaults(value=1, cast=str))
133 ```
134 <div class="result" markdown>
135 ```{.txt .text title="Terminal"}
136 "1"
137 ```
138 !!! success "Conclusion: Successfully casted to `#!py str`."
139 </div>
141 ```pycon {.py .python linenums="1" title="Example 6: Cast to string `'str'`"}
142 >>> print(defaults(value=1, cast="str"))
143 ```
144 <div class="result" markdown>
145 ```{.txt .text title="Terminal"}
146 "1"
147 ```
148 !!! success "Conclusion: Successfully casted to `#!py str`."
149 !!! observation "Note: The only difference between this and the previous example is the type of the `cast` parameter. Here, it is a string representation of the type, whereas in the previous example, we parse'ed in the actual `str` class."
150 </div>
152 ```pycon {.py .python linenums="1" title="Example 7: Invalid cast type"}
153 >>> print(defaults(value="next", cast="bad_type"))
154 ```
155 <div class="result" markdown>
156 ```{.txt .text title="Terminal"}
157 AttributeError: The value for `type` is invalid: `bad_type`.
158 Must be a valid type: ['bool', 'dict', 'int', 'float', 'list', 'str', 'tuple']
159 ```
160 !!! failure "Conclusion: Invalid cast type."
161 </div>
163 ```pycon {.py .python linenums="1" title="Example 8: All blank values"}
164 >>> print(defaults(value=None, cast=None))
165 ```
166 <div class="result" markdown>
167 ```{.txt .text title="Terminal"}
168 AttributeError: Both `value` and `default` are blank: 'None', 'None'.
169 If `value` is blank, then `default` cannot be blank.
170 ```
171 !!! failure "Conclusion: Both `value` and `default` are blank."
172 </div>
174 ??? success "Credit"
175 Inspiration from:<br>
176 https://github.com/henriquebastos/python-decouple/
177 """
179 def __init__(self) -> None:
180 """
181 !!! note "Summary"
182 Nothing is initialised when this class is instantiated.
183 Use the [`__call__()`][toolbox_python.defaults.Defaults.__call__] method instead.
185 ??? tip "See Also"
186 - [`Defaults.__call__()`][toolbox_python.defaults.Defaults.__call__]
187 """
188 return None
190 def __call__(self, *args, **kwargs) -> Any:
191 """
192 !!! note "Summary"
193 When this class is called, it will pass through all parameters to the internal [`.get()`][toolbox_python.defaults.Defaults.get] method.
195 ??? tip "See Also"
196 - [`Defaults.get()`][toolbox_python.defaults.Defaults.get]
197 """
198 return self.get(*args, **kwargs)
200 @typechecked
201 def get(
202 self,
203 value: Any,
204 default: Optional[Any] = None,
205 cast: Optional[Union[str, type]] = None,
206 ) -> Any:
207 """
208 !!! note "Summary"
209 From the value that is parsed in to the `value` parameter, convert it to `default` if `value` is `#!py None`, and convert it to `cast` if `cast` is not `#!py None`.
211 ???+ abstract "Details"
212 The detailed steps will be:
214 1. Validate the input (using the internal [`._validate_value_and_default()`][toolbox_python.defaults.Defaults._validate_value_and_default] & [`._validate_type()`][toolbox_python.defaults.Defaults._validate_type] methods),
215 1. If `value` is `#!py None`, then assign `default` to `value`.
216 1. If `cast` is _not_ `#!py None`, then cast `value` to the data type in `cast`.
217 - Note, `cast` can be _either_ the actual type to convert to, _or_ a string representation of the type.
218 1. Return the updated/defaulted/casted `value` back to the user.
220 Params:
221 value (Any):
222 The value to check.
223 default (Optional[Any], optional):
224 The default value for `value`.<br>
225 Note, can be a `#!py None` value; however, if `value` is also `#!py None`, then `default` _cannot_ be `#!py None`.<br>
226 Defaults to `#!py None`.
227 cast (Optional[Union[str, type]], optional):
228 The data type to convert to.<br>
229 Must be one of: `#!py ["bool", "dict", "int", "float", "list", "str", "tuple"]`.<br>
230 Defaults to `#!py None`.
232 Raises:
233 (TypeCheckError):
234 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.
236 Returns:
237 value (Any):
238 The updated/defaulted/casted value.
240 ???+ example "Examples"
241 ```pycon {.py .python linenums="1" title="Prepare data for examples"}
242 >>> from toolbox_python.defaults import Defaults
243 >>> defaults = Defaults()
244 ```
246 ```pycon {.py .python linenums="1" title="Example 1: Call direct from class"}
247 >>> print(Defaults()(value="this"))
248 ```
249 <div class="result" markdown>
250 ```{.txt .text title="Terminal"}
251 "this"
252 ```
253 !!! success "Conclusion: Successfully printed default value direct from class."
254 </div>
256 ```pycon {.py .python linenums="1" title="Example 2: Call from instantiated class"}
257 >>> print(defaults(value="that"))
258 ```
259 <div class="result" markdown>
260 ```{.txt .text title="Terminal"}
261 "that"
262 ```
263 !!! success "Conclusion: Successfully printed default value from instantiated class."
264 </div>
266 ```pycon {.py .python linenums="1" title="Example 3: Cast to `bool`"}
267 >>> print(defaults(value="True", cast=bool))
268 ```
269 <div class="result" markdown>
270 ```{.txt .text title="Terminal"}
271 True
272 ```
273 !!! success "Conclusion: Successfully casted to `#!py bool`."
274 </div>
276 ```pycon {.py .python linenums="1" title="Example 4: Cast to `int`"}
277 >>> print(defaults(value="1", cast=int))
278 ```
279 <div class="result" markdown>
280 ```{.txt .text title="Terminal"}
281 1
282 ```
283 !!! success "Conclusion: Successfully casted to `#!py int`."
284 </div>
286 ```pycon {.py .python linenums="1" title="Example 5: Cast to `str`"}
287 >>> print(defaults(value=1, cast=str))
288 ```
289 <div class="result" markdown>
290 ```{.txt .text title="Terminal"}
291 "1"
292 ```
293 !!! success "Conclusion: Successfully casted to `#!py str`."
294 </div>
296 ```pycon {.py .python linenums="1" title="Example 6: Cast to string `'str'`"}
297 >>> print(defaults(value=1, cast="str"))
298 ```
299 <div class="result" markdown>
300 ```{.txt .text title="Terminal"}
301 "1"
302 ```
303 !!! success "Conclusion: Successfully casted to `#!py str`."
304 !!! observation "Note: The only difference between this and the previous example is the type of the `cast` parameter. Here, it is a string representation of the type, whereas in the previous example, we parse'ed in the actual `str` class."
305 </div>
307 ```pycon {.py .python linenums="1" title="Example 7: Invalid cast type"}
308 >>> print(defaults(value="next", cast="bad_type"))
309 ```
310 <div class="result" markdown>
311 ```{.txt .text title="Terminal"}
312 AttributeError: The value for `type` is invalid: `bad_type`.
313 Must be a valid type: ['bool', 'dict', 'int', 'float', 'list', 'str', 'tuple']
314 ```
315 !!! failure "Conclusion: Invalid cast type."
316 </div>
318 ```pycon {.py .python linenums="1" title="Example 8: All blank values"}
319 >>> print(defaults(value=None, cast=None))
320 ```
321 <div class="result" markdown>
322 ```{.txt .text title="Terminal"}
323 AttributeError: Both `value` and `default` are blank: 'None', 'None'.
324 If `value` is blank, then `default` cannot be blank.
325 ```
326 !!! failure "Conclusion: Both `value` and `default` are blank."
327 </div>
329 ??? tip "See Also"
330 - [`Defaults._validate_value_and_default()`][toolbox_python.defaults.Defaults._validate_value_and_default]
331 - [`Defaults._validate_type()`][toolbox_python.defaults.Defaults._validate_type]
332 """
333 self._validate_value_and_default(value=value, default=default)._validate_type(check_type=cast)
334 if value is None:
335 value = default
336 if cast is not None:
337 if (cast is bool or cast == "bool") and isinstance(value, str):
338 value = bool(strtobool(value))
339 elif isinstance(cast, str):
340 value = eval(cast)(value)
341 else:
342 value = cast(value)
343 return value
345 def _validate_value_and_default(
346 self,
347 value: Optional[Any] = None,
348 default: Optional[Any] = None,
349 ) -> Defaults:
350 """
351 !!! note "Summary"
352 Validate to ensure that `value` and `default` are not both `#!py None`.
354 Params:
355 value (Optional[Any], optional):
356 The `value` to check.<br>
357 Defaults to `#!py None`.
358 default (Optional[Any], optional):
359 The `default` value to check.<br>
360 Defaults to `#!py None`.
362 Raises:
363 (AttributeError): If both `value` and `default` are `#!py None`.
365 Returns:
366 self (Defaults):
367 If both `value` and `default` are not both `#!py None`, then return `self`.
369 ??? tip "See Also"
370 - [`Defaults.get()`][toolbox_python.defaults.Defaults.get]
371 """
372 if value is None and default is None:
373 raise AttributeError(
374 f"Both `value` and `default` are blank: '{value}', '{default}'.\n"
375 f"If `value` is blank, then `default` cannot be blank."
376 )
377 return self
379 def _validate_type(
380 self,
381 check_type: Optional[Union[str, type]] = None,
382 ) -> Defaults:
383 """
384 !!! note "Summary"
385 Check to ensure that `check_type` is a valid Python type.<br>
386 Must be one of: `#!py ["bool", "dict", "int", "float", "list", "str", "tuple"]`.
388 Params:
389 check_type (Optional[Union[str, type]], optional):
390 The type to check against. Can either be an actual Python type, or it's string representation.<br>
391 Defaults to `#!py None`.
393 Raises:
394 (AttributeError): If `check_type` is _both_ not `#!py None` _and_ if it is not one of the valid Python types.
396 Returns:
397 self (Defaults):
398 If the type is valid, return `self`.
400 ??? tip "See Also"
401 - [`Defaults.get()`][toolbox_python.defaults.Defaults.get]
402 """
403 valid_types: list[str] = [
404 "bool",
405 "dict",
406 "int",
407 "float",
408 "list",
409 "str",
410 "tuple",
411 ]
412 retype: str | type | None = None
413 if check_type is None:
414 return self
415 elif is_type(check_type, str):
416 retype = check_type
417 elif type(check_type).__name__ == "type":
418 retype = check_type.__name__ # type: ignore
419 if retype is not None and retype not in valid_types:
420 raise AttributeError(
421 f"The value for `type` is invalid: `{retype}`.\n" f"Must be a valid type: {valid_types}."
422 )
423 return self
426# ---------------------------------------------------------------------------- #
427# Instantiations ####
428# ---------------------------------------------------------------------------- #
431defaults = Defaults()