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

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 ???+ example "Examples" 

80 

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

82 >>> from toolbox_python.defaults import Defaults 

83 >>> defaults = Defaults() 

84 ``` 

85 

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> 

95 

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> 

105 

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> 

115 

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> 

125 

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> 

135 

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> 

146 

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> 

157 

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> 

168 

169 ??? success "Credit" 

170 Inspiration from:<br> 

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

172 """ 

173 

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. 

179 

180 ??? tip "See Also" 

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

182 """ 

183 return None 

184 

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. 

189 

190 ??? tip "See Also" 

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

192 """ 

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

194 

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

205 

206 ???+ info "Details" 

207 The detailed steps will be: 

208 

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. 

214 

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

226 

227 Returns: 

228 value (Any): 

229 The updated/defaulted/casted value. 

230 

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 

250 

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

259 

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

267 

268 Raises: 

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

270 

271 Returns: 

272 self (Defaults): 

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

274 

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 

284 

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

293 

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

298 

299 Raises: 

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

301 

302 Returns: 

303 self (Defaults): 

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

305 

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 

330 

331 

332# ---------------------------------------------------------------------------- # 

333# Instantiations #### 

334# ---------------------------------------------------------------------------- # 

335 

336 

337defaults = Defaults()