Coverage for src / ts_stat_tests / utils / errors.py: 100%

29 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-02-01 09:48 +0000

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

2# # 

3# Title: Error Utilities # 

4# Purpose: Functions for generating standardized error messages and # 

5# performing data equality checks. # 

6# # 

7# ============================================================================ # 

8 

9 

10# ---------------------------------------------------------------------------- # 

11# # 

12# Overview #### 

13# # 

14# ---------------------------------------------------------------------------- # 

15 

16 

17# ---------------------------------------------------------------------------- # 

18# Description #### 

19# ---------------------------------------------------------------------------- # 

20 

21 

22""" 

23!!! note "Summary" 

24 

25 Provides utility functions for generating standardized error messages and performing numeric assertions. 

26 

27 This module includes functions to format error messages consistently and check if numeric values are within a specified tolerance, which is useful for testing and validation purposes. 

28""" 

29 

30 

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

32# # 

33# Setup #### 

34# # 

35# ---------------------------------------------------------------------------- # 

36 

37 

38## --------------------------------------------------------------------------- # 

39## Imports #### 

40## --------------------------------------------------------------------------- # 

41 

42 

43# ## Python StdLib Imports ---- 

44from typing import Mapping, Optional, Union, overload 

45 

46# ## Python Third Party Imports ---- 

47from typeguard import typechecked 

48 

49 

50# ---------------------------------------------------------------------------- # 

51# # 

52# Functions #### 

53# # 

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

55 

56 

57## --------------------------------------------------------------------------- # 

58## Error messages #### 

59## --------------------------------------------------------------------------- # 

60 

61 

62@typechecked 

63def generate_error_message( 

64 parameter_name: str, 

65 value_parsed: str, 

66 options: Union[Mapping[str, Union[tuple[str, ...], list[str], str]], tuple[str, ...], list[str]], 

67) -> str: 

68 r""" 

69 !!! note "Summary" 

70 Generates a formatted error message for mismatched values or invalid options. 

71 

72 ???+ abstract "Details" 

73 This function constructs a standardized string that describes a mismatch between a provided value and the allowed options for a given parameter. It is primarily used to provide clear, consistent feedback in `ValueError` exceptions within dispatchers. 

74 

75 Params: 

76 parameter_name (str): 

77 The name of the parameter or variable being checked. 

78 value_parsed (str): 

79 The actual value that was received. 

80 options (Union[Mapping[str, Union[tuple[str, ...], list[str], str]], tuple[str, ...], list[str]]): 

81 The set of valid options or a dictionary mapping categories to valid options. 

82 

83 Returns: 

84 (str): 

85 A formatted error message string. 

86 

87 ???+ example "Examples" 

88 

89 ```pycon {.py .python linenums="1" title="Setup"} 

90 >>> from ts_stat_tests.utils.errors import generate_error_message 

91 

92 ``` 

93 

94 ```pycon {.py .python linenums="1" title="Example 1: Simple Options"} 

95 >>> msg = generate_error_message("param", "invalid", ["opt1", "opt2"]) 

96 >>> print(msg) 

97 Invalid 'param': invalid. Options: ['opt1', 'opt2'] 

98 

99 ``` 

100 

101 ??? question "References" 

102 1. [Python F-Strings documentation](https://docs.python.org/3/reference/lexical_analysis.html#f-strings) 

103 

104 """ 

105 return f"Invalid '{parameter_name}': {value_parsed}. Options: {options}" 

106 

107 

108@overload 

109def is_almost_equal(first: float, second: float, *, places: int = 7) -> bool: ... 

110@overload 

111def is_almost_equal(first: float, second: float, *, delta: float) -> bool: ... 

112@typechecked 

113def is_almost_equal( 

114 first: float, 

115 second: float, 

116 *, 

117 places: Optional[int] = None, 

118 delta: Optional[float] = None, 

119) -> bool: 

120 r""" 

121 !!! note "Summary" 

122 Checks if two float values are almost equal within a specified precision. 

123 

124 ???+ abstract "Details" 

125 Determines the equality of two floating-point numbers within a tolerance. This is necessary because floating-point arithmetic can introduce small errors that make direct equality comparisons (e.g., `a == b`) unreliable. 

126 

127 The user can specify tolerance either by `places` (decimal places) or by an absolute `delta`. 

128 

129 Params: 

130 first (float): 

131 The first float value. 

132 second (float): 

133 The second float value. 

134 places (Optional[int]): 

135 The number of decimal places for comparison. Defaults to 7 if not provided. 

136 delta (Optional[float]): 

137 An optional delta value for comparison. 

138 

139 Raises: 

140 (ValueError): 

141 If both `places` and `delta` are provided. 

142 

143 Returns: 

144 (bool): 

145 `True` if the values are almost equal, `False` otherwise. 

146 

147 ???+ example "Examples" 

148 

149 ```pycon {.py .python linenums="1" title="Setup"} 

150 >>> from ts_stat_tests.utils.errors import is_almost_equal 

151 

152 ``` 

153 

154 ```pycon {.py .python linenums="1" title="Example 1: Using `places`"} 

155 >>> res_places = is_almost_equal(1.0, 1.00000001, places=7) 

156 >>> print(res_places) 

157 True 

158 

159 ``` 

160 

161 ```pycon {.py .python linenums="1" title="Example 2: Using `delta`"} 

162 >>> res_delta = is_almost_equal(1.0, 1.1, delta=0.2) 

163 >>> print(res_delta) 

164 True 

165 

166 ``` 

167 

168 ```pycon {.py .python linenums="1" title="Example 3: Not Almost Equal"} 

169 >>> res_not_equal = is_almost_equal(1.0, 1.1, places=3) 

170 >>> print(res_not_equal) 

171 False 

172 

173 ``` 

174 

175 ??? equation "Calculation" 

176 

177 The comparison depends on whether `delta` or `places` is provided. 

178 

179 If `delta` is specified: 

180 

181 $$ 

182 |first - second| \le \text{delta} 

183 $$ 

184 

185 If `places` is specified (defaults to 7): 

186 

187 $$ 

188 \text{round}(|first - second|, \text{places}) = 0 

189 $$ 

190 

191 ??? success "Credit" 

192 Inspiration from Python's UnitTest function [`assertAlmostEqual`](https://github.com/python/cpython/blob/3.11/Lib/unittest/case.py). 

193 

194 ??? question "References" 

195 1. [Python unittest source code](https://github.com/python/cpython/blob/main/Lib/unittest/case.py) 

196 

197 """ 

198 if places is not None and delta is not None: 

199 raise ValueError("Specify `delta` or `places`, not both.") 

200 if first == second: 

201 return True 

202 diff: float = abs(first - second) 

203 if delta is not None: 

204 if diff <= delta: 

205 return True 

206 else: 

207 places_val: int = places if places is not None else 7 

208 if round(diff, places_val) == 0: 

209 return True 

210 return False 

211 

212 

213@overload 

214def assert_almost_equal(first: float, second: float, msg: Optional[str] = None, *, places: int = 7) -> None: ... 

215@overload 

216def assert_almost_equal(first: float, second: float, msg: Optional[str] = None, *, delta: float) -> None: ... 

217@typechecked 

218def assert_almost_equal( 

219 first: float, 

220 second: float, 

221 msg: Optional[str] = None, 

222 *, 

223 places: Optional[int] = None, 

224 delta: Optional[float] = None, 

225) -> None: 

226 r""" 

227 !!! note "Summary" 

228 Asserts that two float values are almost equal within a specified precision. 

229 

230 ???+ abstract "Details" 

231 Performs a floating-point comparison using [is_almost_equal][ts_stat_tests.utils.errors.is_almost_equal]. If the comparison fails, an `AssertionError` is raised with a descriptive message. 

232 

233 Params: 

234 first (float): 

235 The first float value. 

236 second (float): 

237 The second float value. 

238 msg (Optional[str]): 

239 An optional message to include in the exception if the values are not almost equal. 

240 places (Optional[int]): 

241 The number of decimal places for comparison. Defaults to 7 if not provided. 

242 delta (Optional[float]): 

243 An optional delta value for comparison. 

244 

245 Raises: 

246 (ValueError): 

247 If both `places` and `delta` are provided. 

248 (AssertionError): 

249 If the two float values are not almost equal. 

250 

251 Returns: 

252 (None): 

253 None. Raises an `AssertionError` if the values are not almost equal to within the tolerances specified. 

254 

255 ???+ example "Examples" 

256 

257 ```pycon {.py .python linenums="1" title="Setup"} 

258 >>> from ts_stat_tests.utils.errors import assert_almost_equal 

259 

260 ``` 

261 

262 ```pycon {.py .python linenums="1" title="Example 1: Using `places`"} 

263 >>> res_places = assert_almost_equal(1.0, 1.0, places=7) 

264 >>> print(res_places is None) 

265 True 

266 

267 ``` 

268 

269 ```pycon {.py .python linenums="1" title="Example 2: Using `delta`"} 

270 >>> res_delta = assert_almost_equal(1.0, 1.1, delta=0.2) 

271 >>> print(res_delta is None) 

272 True 

273 

274 ``` 

275 

276 ```pycon {.py .python linenums="1" title="Example 3: AssertionError Raised"} 

277 >>> assert_almost_equal(1.0, 1.1, places=3) 

278 Traceback (most recent call last): 

279 ... 

280 AssertionError: Assertion failed: 1.0 != 1.1 (places=3, delta=None) 

281 

282 ``` 

283 

284 ??? equation "Calculation" 

285 

286 Refer to [is_almost_equal][ts_stat_tests.utils.errors.is_almost_equal] for the underlying logic. 

287 

288 ??? success "Credit" 

289 Inspiration from Python's UnitTest function [`assertAlmostEqual`](https://github.com/python/cpython/blob/3.11/Lib/unittest/case.py). 

290 

291 ??? question "References" 

292 1. [Python unittest source code](https://github.com/python/cpython/blob/main/Lib/unittest/case.py) 

293 

294 """ 

295 is_equal: bool = False 

296 if delta is not None: 

297 is_equal = is_almost_equal(first, second, delta=delta) 

298 else: 

299 places_val: int = places if places is not None else 7 

300 is_equal = is_almost_equal(first, second, places=places_val) 

301 

302 if not is_equal: 

303 error_msg: str = msg if msg is not None else f"Assertion failed: {first} != {second} ({places=}, {delta=})" 

304 raise AssertionError(error_msg)