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

1# ============================================================================ # 

2# # 

3# Title : Defaults # 

4# Purpose : Enable setting and utilisation of default values. # 

5# # 

6# ============================================================================ # 

7 

8 

9# ---------------------------------------------------------------------------- # 

10# # 

11# Overview #### 

12# # 

13# ---------------------------------------------------------------------------- # 

14 

15 

16# ---------------------------------------------------------------------------- # 

17# Description #### 

18# ---------------------------------------------------------------------------- # 

19 

20 

21""" 

22!!! note "Summary" 

23 The `defaults` module is used how to set and control default values for our various Python processes. 

24""" 

25 

26 

27# ---------------------------------------------------------------------------- # 

28# # 

29# Setup #### 

30# # 

31# ---------------------------------------------------------------------------- # 

32 

33 

34# ---------------------------------------------------------------------------- # 

35# Imports #### 

36# ---------------------------------------------------------------------------- # 

37 

38 

39# ## Future Python Library Imports ---- 

40from __future__ import annotations 

41 

42# ## Python StdLib Imports ---- 

43from typing import Any, Optional, Union 

44 

45# ## Python Third Party Imports ---- 

46from typeguard import typechecked 

47 

48# ## Local First Party Imports ---- 

49from toolbox_python.bools import strtobool 

50from toolbox_python.checkers import is_type 

51 

52 

53# ---------------------------------------------------------------------------- # 

54# Exports #### 

55# ---------------------------------------------------------------------------- # 

56 

57 

58__all__: list[str] = ["defaults", "Defaults"] 

59 

60 

61# ---------------------------------------------------------------------------- # 

62# # 

63# Classes #### 

64# # 

65# ---------------------------------------------------------------------------- # 

66 

67 

68# ---------------------------------------------------------------------------- # 

69# Defaults Class #### 

70# ---------------------------------------------------------------------------- # 

71 

72 

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. 

78 

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 

83 

84 ???+ example "Examples" 

85 

86 ```pycon {.py .python linenums="1" title="Set up data for examples"} 

87 >>> from toolbox_python.defaults import Defaults 

88 >>> defaults = Defaults() 

89 ``` 

90 

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> 

100 

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> 

110 

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> 

120 

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> 

130 

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> 

140 

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> 

151 

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> 

162 

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> 

173 

174 ??? success "Credit" 

175 Inspiration from:<br> 

176 https://github.com/henriquebastos/python-decouple/ 

177 """ 

178 

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. 

184 

185 ??? tip "See Also" 

186 - [`Defaults.__call__()`][toolbox_python.defaults.Defaults.__call__] 

187 """ 

188 return None 

189 

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. 

194 

195 ??? tip "See Also" 

196 - [`Defaults.get()`][toolbox_python.defaults.Defaults.get] 

197 """ 

198 return self.get(*args, **kwargs) 

199 

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`. 

210 

211 ???+ abstract "Details" 

212 The detailed steps will be: 

213 

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. 

219 

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`. 

231 

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. 

235 

236 Returns: 

237 value (Any): 

238 The updated/defaulted/casted value. 

239 

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 ``` 

245 

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> 

255 

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> 

265 

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> 

275 

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> 

285 

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> 

295 

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> 

306 

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> 

317 

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> 

328 

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 

344 

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`. 

353 

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`. 

361 

362 Raises: 

363 (AttributeError): If both `value` and `default` are `#!py None`. 

364 

365 Returns: 

366 self (Defaults): 

367 If both `value` and `default` are not both `#!py None`, then return `self`. 

368 

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 

378 

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"]`. 

387 

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`. 

392 

393 Raises: 

394 (AttributeError): If `check_type` is _both_ not `#!py None` _and_ if it is not one of the valid Python types. 

395 

396 Returns: 

397 self (Defaults): 

398 If the type is valid, return `self`. 

399 

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 

424 

425 

426# ---------------------------------------------------------------------------- # 

427# Instantiations #### 

428# ---------------------------------------------------------------------------- # 

429 

430 

431defaults = Defaults()