Coverage for src/toolbox_python/defaults.py: 100%
39 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 : 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 ???+ example "Examples"
81 ```{.py .python linenums="1" title="Set up data for examples"}
82 >>> from toolbox_python.defaults import Defaults
83 >>> defaults = Defaults()
84 ```
86 ```{.py .python linenums="1" title="Example 1: Call direct from class"}
87 >>> print(Defaults()(value="this"))
88 ```
89 <div class="result" markdown>
90 ```{.txt .text title="Terminal"}
91 "this"
92 ```
93 !!! success "Conclusion: Successfully printed default value direct from class."
94 </div>
96 ```{.py .python linenums="1" title="Example 2: Call from instantiated class"}
97 >>> print(defaults(value="that"))
98 ```
99 <div class="result" markdown>
100 ```{.txt .text title="Terminal"}
101 "that"
102 ```
103 !!! success "Conclusion: Successfully printed default value from instantiated class."
104 </div>
106 ```{.py .python linenums="1" title="Example 3: Cast to `bool`"}
107 >>> print(defaults(value="True", cast=bool))
108 ```
109 <div class="result" markdown>
110 ```{.txt .text title="Terminal"}
111 True
112 ```
113 !!! success "Conclusion: Successfully casted to `#!py bool`."
114 </div>
116 ```{.py .python linenums="1" title="Example 4: Cast to `int`"}
117 >>> print(defaults(value="1", cast=int))
118 ```
119 <div class="result" markdown>
120 ```{.txt .text title="Terminal"}
121 1
122 ```
123 !!! success "Conclusion: Successfully casted to `#!py int`."
124 </div>
126 ```{.py .python linenums="1" title="Example 5: Cast to `str`"}
127 >>> print(defaults(value=1, cast=str))
128 ```
129 <div class="result" markdown>
130 ```{.txt .text title="Terminal"}
131 "1"
132 ```
133 !!! success "Conclusion: Successfully casted to `#!py str`."
134 </div>
136 ```{.py .python linenums="1" title="Example 6: Cast to string `'str'`"}
137 >>> print(defaults(value=1, cast="str"))
138 ```
139 <div class="result" markdown>
140 ```{.txt .text title="Terminal"}
141 "1"
142 ```
143 !!! success "Conclusion: Successfully casted to `#!py str`."
144 !!! 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."
145 </div>
147 ```{.py .python linenums="1" title="Example 7: Invalid cast type"}
148 >>> print(defaults(value="next", cast="bad_type"))
149 ```
150 <div class="result" markdown>
151 ```{.txt .text title="Terminal"}
152 AttributeError: The value for `type` is invalid: `bad_type`.
153 Must be a valid type: ['bool', 'dict', 'int', 'float', 'list', 'str', 'tuple']
154 ```
155 !!! failure "Conclusion: Invalid cast type."
156 </div>
158 ```{.py .python linenums="1" title="Example 8: All blank values"}
159 >>> print(defaults(value=None, cast=None))
160 ```
161 <div class="result" markdown>
162 ```{.txt .text title="Terminal"}
163 AttributeError: Both `value` and `default` are blank: 'None', 'None'.
164 If `value` is blank, then `default` cannot be blank.
165 ```
166 !!! failure "Conclusion: Both `value` and `default` are blank."
167 </div>
169 ??? success "Credit"
170 Inspiration from:<br>
171 https://github.com/henriquebastos/python-decouple/
172 """
174 def __init__(self) -> None:
175 """
176 !!! note "Summary"
177 Nothing is initialised when this class is instantiated.
178 Use the [`__call__()`][toolbox_python.defaults.Defaults.__call__] method instead.
180 ??? tip "See Also"
181 - [`Defaults.__call__()`][toolbox_python.defaults.Defaults.__call__]
182 """
183 return None
185 def __call__(self, *args, **kwargs) -> Any:
186 """
187 !!! note "Summary"
188 When this class is called, it will pass through all parameters to the internal [`.get()`][toolbox_python.defaults.Defaults.get] method.
190 ??? tip "See Also"
191 - [`Defaults.get()`][toolbox_python.defaults.Defaults.get]
192 """
193 return self.get(*args, **kwargs)
195 @typechecked
196 def get(
197 self,
198 value: Any,
199 default: Optional[Any] = None,
200 cast: Optional[Union[str, type]] = None,
201 ) -> Any:
202 """
203 !!! note "Summary"
204 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`.
206 ???+ info "Details"
207 The detailed steps will be:
209 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),
210 1. If `value` is `#!py None`, then assign `default` to `value`.
211 1. If `cast` is _not_ `#!py None`, then cast `value` to the data type in `cast`.
212 - Note, `cast` can be _either_ the actual type to convert to, _or_ a string representation of the type.
213 1. Return the updated/defaulted/casted `value` back to the user.
215 Params:
216 value (Any):
217 The value to check.
218 default (Optional[Any], optional):
219 The default value for `value`.<br>
220 Note, can be a `#!py None` value; however, if `value` is also `#!py None`, then `default` _cannot_ be `#!py None`.<br>
221 Defaults to `#!py None`.
222 cast (Optional[Union[str, type]], optional):
223 The data type to convert to.<br>
224 Must be one of: `#!py ["bool", "dict", "int", "float", "list", "str", "tuple"]`.<br>
225 Defaults to `#!py None`.
227 Returns:
228 value (Any):
229 The updated/defaulted/casted value.
231 ??? tip "See Also"
232 - [`Defaults._validate_value_and_default()`][toolbox_python.defaults.Defaults._validate_value_and_default]
233 - [`Defaults._validate_type()`][toolbox_python.defaults.Defaults._validate_type]
234 """
235 (
236 self._validate_value_and_default(
237 value=value, default=default
238 )._validate_type(check_type=cast)
239 )
240 if value is None:
241 value = default
242 if cast is not None:
243 if (cast is bool or cast == "bool") and is_type(value, str):
244 value = bool(strtobool(value))
245 elif isinstance(cast, str):
246 value = eval(cast)(value)
247 else:
248 value = cast(value)
249 return value
251 def _validate_value_and_default(
252 self,
253 value: Optional[Any] = None,
254 default: Optional[Any] = None,
255 ) -> Defaults:
256 """
257 !!! note "Summary"
258 Validate to ensure that `value` and `default` are not both `#!py None`.
260 Params:
261 value (Optional[Any], optional):
262 The `value` to check.<br>
263 Defaults to `#!py None`.
264 default (Optional[Any], optional):
265 The `default` value to check.<br>
266 Defaults to `#!py None`.
268 Raises:
269 AttributeError: If both `value` and `default` are `#!py None`.
271 Returns:
272 self (Defaults):
273 If both `value` and `default` are not both `#!py None`, then return `self`.
275 ??? tip "See Also"
276 - [`Defaults.get()`][toolbox_python.defaults.Defaults.get]
277 """
278 if value is None and default is None:
279 raise AttributeError(
280 f"Both `value` and `default` are blank: '{value}', '{default}'.\n"
281 f"If `value` is blank, then `default` cannot be blank."
282 )
283 return self
285 def _validate_type(
286 self,
287 check_type: Optional[Union[str, type]] = None,
288 ) -> Defaults:
289 """
290 !!! note "Summary"
291 Check to ensure that `check_type` is a valid Python type.<br>
292 Must be one of: `#!py ["bool", "dict", "int", "float", "list", "str", "tuple"]`.
294 Params:
295 check_type (Optional[Union[str, type]], optional):
296 The type to check against. Can either be an actual Python type, or it's string representation.<br>
297 Defaults to `#!py None`.
299 Raises:
300 AttributeError: If `check_type` is _both_ not `#!py None` _and_ if it is not one of the valid Python types.
302 Returns:
303 self (Defaults):
304 If the type is valid, return `self`.
306 ??? tip "See Also"
307 - [`Defaults.get()`][toolbox_python.defaults.Defaults.get]
308 """
309 valid_types: list[str] = [
310 "bool",
311 "dict",
312 "int",
313 "float",
314 "list",
315 "str",
316 "tuple",
317 ]
318 if check_type is None:
319 return self
320 elif is_type(check_type, str):
321 retype = check_type
322 elif type(check_type).__name__ == "type":
323 retype = check_type.__name__ # type: ignore
324 if retype is not None and retype not in valid_types:
325 raise AttributeError(
326 f"The value for `type` is invalid: `{retype}`.\n"
327 f"Must be a valid type: {valid_types}."
328 )
329 return self
332# ---------------------------------------------------------------------------- #
333# Instantiations ####
334# ---------------------------------------------------------------------------- #
337defaults = Defaults()