Coverage for src/toolbox_python/defaults.py: 100%
40 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 : 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
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
51from toolbox_python.collection_types import str_list
54# ---------------------------------------------------------------------------- #
55# Exports ####
56# ---------------------------------------------------------------------------- #
59__all__: str_list = ["defaults", "Defaults"]
62# ---------------------------------------------------------------------------- #
63# #
64# Classes ####
65# #
66# ---------------------------------------------------------------------------- #
69# ---------------------------------------------------------------------------- #
70# Defaults Class ####
71# ---------------------------------------------------------------------------- #
74class Defaults:
75 """
76 !!! note "Summary"
77 When we create and use Python variables, it is sometimes handy to add a default value for that variable.
78 This class will handle that process.
80 ???+ example "Examples"
82 ```{.py .python linenums="1" title="Set up data for examples"}
83 >>> from toolbox_python.defaults import Defaults
84 >>> defaults = Defaults()
85 ```
87 ```{.py .python linenums="1" title="Example 1: Call direct from class"}
88 >>> print(Defaults()(value="this"))
89 ```
90 <div class="result" markdown>
91 ```{.txt .text title="Terminal"}
92 "this"
93 ```
94 !!! success "Conclusion: Successfully printed default value direct from class."
95 </div>
97 ```{.py .python linenums="1" title="Example 2: Call from instantiated class"}
98 >>> print(defaults(value="that"))
99 ```
100 <div class="result" markdown>
101 ```{.txt .text title="Terminal"}
102 "that"
103 ```
104 !!! success "Conclusion: Successfully printed default value from instantiated class."
105 </div>
107 ```{.py .python linenums="1" title="Example 3: Cast to `bool`"}
108 >>> print(defaults(value="True", cast=bool))
109 ```
110 <div class="result" markdown>
111 ```{.txt .text title="Terminal"}
112 True
113 ```
114 !!! success "Conclusion: Successfully casted to `#!py bool`."
115 </div>
117 ```{.py .python linenums="1" title="Example 4: Cast to `int`"}
118 >>> print(defaults(value="1", cast=int))
119 ```
120 <div class="result" markdown>
121 ```{.txt .text title="Terminal"}
122 1
123 ```
124 !!! success "Conclusion: Successfully casted to `#!py int`."
125 </div>
127 ```{.py .python linenums="1" title="Example 5: Cast to `str`"}
128 >>> print(defaults(value=1, cast=str))
129 ```
130 <div class="result" markdown>
131 ```{.txt .text title="Terminal"}
132 "1"
133 ```
134 !!! success "Conclusion: Successfully casted to `#!py str`."
135 </div>
137 ```{.py .python linenums="1" title="Example 6: Cast to string `'str'`"}
138 >>> print(defaults(value=1, cast="str"))
139 ```
140 <div class="result" markdown>
141 ```{.txt .text title="Terminal"}
142 "1"
143 ```
144 !!! success "Conclusion: Successfully casted to `#!py str`."
145 !!! 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."
146 </div>
148 ```{.py .python linenums="1" title="Example 7: Invalid cast type"}
149 >>> print(defaults(value="next", cast="bad_type"))
150 ```
151 <div class="result" markdown>
152 ```{.txt .text title="Terminal"}
153 AttributeError: The value for `type` is invalid: `bad_type`.
154 Must be a valid type: ['bool', 'dict', 'int', 'float', 'list', 'str', 'tuple']
155 ```
156 !!! failure "Conclusion: Invalid cast type."
157 </div>
159 ```{.py .python linenums="1" title="Example 8: All blank values"}
160 >>> print(defaults(value=None, cast=None))
161 ```
162 <div class="result" markdown>
163 ```{.txt .text title="Terminal"}
164 AttributeError: Both `value` and `default` are blank: 'None', 'None'.
165 If `value` is blank, then `default` cannot be blank.
166 ```
167 !!! failure "Conclusion: Both `value` and `default` are blank."
168 </div>
170 ??? success "Credit"
171 Inspiration from:<br>
172 https://github.com/henriquebastos/python-decouple/
173 """
175 def __init__(self) -> None:
176 """
177 !!! note "Summary"
178 Nothing is initialised when this class is instantiated.
179 Use the [`__call__()`][toolbox_python.defaults.Defaults.__call__] method instead.
181 ??? tip "See Also"
182 - [`Defaults.__call__()`][toolbox_python.defaults.Defaults.__call__]
183 """
184 return None
186 def __call__(self, *args, **kwargs) -> Any:
187 """
188 !!! note "Summary"
189 When this class is called, it will pass through all parameters to the internal [`.get()`][toolbox_python.defaults.Defaults.get] method.
191 ??? tip "See Also"
192 - [`Defaults.get()`][toolbox_python.defaults.Defaults.get]
193 """
194 return self.get(*args, **kwargs)
196 @typechecked
197 def get(
198 self,
199 value: Any,
200 default: Any | None = None,
201 cast: str | type | None = None,
202 ) -> Any:
203 """
204 !!! note "Summary"
205 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`.
207 ???+ info "Details"
208 The detailed steps will be:
210 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),
211 1. If `value` is `#!py None`, then assign `default` to `value`.
212 1. If `cast` is _not_ `#!py None`, then cast `value` to the data type in `cast`.
213 - Note, `cast` can be _either_ the actual type to convert to, _or_ a string representation of the type.
214 1. Return the updated/defaulted/casted `value` back to the user.
216 Params:
217 value (Any):
218 The value to check.
219 default (Optional[Any], optional):
220 The default value for `value`.<br>
221 Note, can be a `#!py None` value; however, if `value` is also `#!py None`, then `default` _cannot_ be `#!py None`.<br>
222 Defaults to `#!py None`.
223 cast (Optional[Union[str, type]], optional):
224 The data type to convert to.<br>
225 Must be one of: `#!py ["bool", "dict", "int", "float", "list", "str", "tuple"]`.<br>
226 Defaults to `#!py None`.
228 Returns:
229 value (Any):
230 The updated/defaulted/casted value.
232 ??? tip "See Also"
233 - [`Defaults._validate_value_and_default()`][toolbox_python.defaults.Defaults._validate_value_and_default]
234 - [`Defaults._validate_type()`][toolbox_python.defaults.Defaults._validate_type]
235 """
236 (
237 self._validate_value_and_default(
238 value=value, default=default
239 )._validate_type(check_type=cast)
240 )
241 if value is None:
242 value = default
243 if cast is not None:
244 if (cast is bool or cast == "bool") and is_type(value, str):
245 value = bool(strtobool(value))
246 elif isinstance(cast, str):
247 value = eval(cast)(value)
248 else:
249 value = cast(value)
250 return value
252 def _validate_value_and_default(
253 self,
254 value: Any | None = None,
255 default: Any | None = None,
256 ) -> Defaults:
257 """
258 !!! note "Summary"
259 Validate to ensure that `value` and `default` are not both `#!py None`.
261 Params:
262 value (Optional[Any], optional):
263 The `value` to check.<br>
264 Defaults to `#!py None`.
265 default (Optional[Any], optional):
266 The `default` value to check.<br>
267 Defaults to `#!py None`.
269 Raises:
270 AttributeError: If both `value` and `default` are `#!py None`.
272 Returns:
273 self (Defaults):
274 If both `value` and `default` are not both `#!py None`, then return `self`.
276 ??? tip "See Also"
277 - [`Defaults.get()`][toolbox_python.defaults.Defaults.get]
278 """
279 if value is None and default is None:
280 raise AttributeError(
281 f"Both `value` and `default` are blank: '{value}', '{default}'.\n"
282 f"If `value` is blank, then `default` cannot be blank."
283 )
284 return self
286 def _validate_type(
287 self,
288 check_type: str | type | None = None,
289 ) -> Defaults:
290 """
291 !!! note "Summary"
292 Check to ensure that `check_type` is a valid Python type.<br>
293 Must be one of: `#!py ["bool", "dict", "int", "float", "list", "str", "tuple"]`.
295 Params:
296 check_type (Optional[Union[str, type]], optional):
297 The type to check against. Can either be an actual Python type, or it's string representation.<br>
298 Defaults to `#!py None`.
300 Raises:
301 AttributeError: If `check_type` is _both_ not `#!py None` _and_ if it is not one of the valid Python types.
303 Returns:
304 self (Defaults):
305 If the type is valid, return `self`.
307 ??? tip "See Also"
308 - [`Defaults.get()`][toolbox_python.defaults.Defaults.get]
309 """
310 valid_types: str_list = [
311 "bool",
312 "dict",
313 "int",
314 "float",
315 "list",
316 "str",
317 "tuple",
318 ]
319 if check_type is None:
320 return self
321 elif is_type(check_type, str):
322 retype = check_type
323 elif type(check_type).__name__ == "type":
324 retype = check_type.__name__ # type: ignore
325 if retype is not None and retype not in valid_types:
326 raise AttributeError(
327 f"The value for `type` is invalid: `{retype}`.\n"
328 f"Must be a valid type: {valid_types}."
329 )
330 return self
333# ---------------------------------------------------------------------------- #
334# Instantiations ####
335# ---------------------------------------------------------------------------- #
338defaults = Defaults()