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

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 

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 

51from toolbox_python.collection_types import str_list 

52 

53 

54# ---------------------------------------------------------------------------- # 

55# Exports #### 

56# ---------------------------------------------------------------------------- # 

57 

58 

59__all__: str_list = ["defaults", "Defaults"] 

60 

61 

62# ---------------------------------------------------------------------------- # 

63# # 

64# Classes #### 

65# # 

66# ---------------------------------------------------------------------------- # 

67 

68 

69# ---------------------------------------------------------------------------- # 

70# Defaults Class #### 

71# ---------------------------------------------------------------------------- # 

72 

73 

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. 

79 

80 ???+ example "Examples" 

81 

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

83 >>> from toolbox_python.defaults import Defaults 

84 >>> defaults = Defaults() 

85 ``` 

86 

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> 

96 

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> 

106 

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> 

116 

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> 

126 

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> 

136 

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> 

147 

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> 

158 

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> 

169 

170 ??? success "Credit" 

171 Inspiration from:<br> 

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

173 """ 

174 

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. 

180 

181 ??? tip "See Also" 

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

183 """ 

184 return None 

185 

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. 

190 

191 ??? tip "See Also" 

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

193 """ 

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

195 

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

206 

207 ???+ info "Details" 

208 The detailed steps will be: 

209 

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. 

215 

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

227 

228 Returns: 

229 value (Any): 

230 The updated/defaulted/casted value. 

231 

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 

251 

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

260 

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

268 

269 Raises: 

270 AttributeError: If both `value` and `default` are `#!py None`. 

271 

272 Returns: 

273 self (Defaults): 

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

275 

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 

285 

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

294 

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

299 

300 Raises: 

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

302 

303 Returns: 

304 self (Defaults): 

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

306 

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 

331 

332 

333# ---------------------------------------------------------------------------- # 

334# Instantiations #### 

335# ---------------------------------------------------------------------------- # 

336 

337 

338defaults = Defaults()