Coverage for /home/ubuntu/lunchbox/python/lunchbox/enforce.py: 100%

99 statements  

« prev     ^ index     » next       coverage.py v7.9.0, created at 2025-06-13 03:03 +0000

1from typing import Any, List, Optional, Set, Tuple, Union # noqa: F401 

2 

3from enum import Enum 

4# ------------------------------------------------------------------------------ 

5 

6 

7''' 

8The enforce module contains the Enforce class which is used for faciltating 

9inline testing, inside of function defitions and test definitions. 

10''' 

11 

12 

13class Comparator(Enum): 

14 ''' 

15 Enum for comparison operators used by Enforce. 

16 

17 Includes: 

18 

19 * EQ 

20 * NOT_EQ 

21 * GT 

22 * GTE 

23 * LT 

24 * LTE 

25 * SIMILAR 

26 * NOT_SIMILAR 

27 * IN 

28 * NOT_IN 

29 * INSTANCE_OF 

30 * NOT_INSTANCE_OF 

31 ''' 

32 EQ = ('eq', 'equal', '==', False, '!=', 'not equal to' ) # noqa: E241, E202, E501, E221 

33 NOT_EQ = ('eq', 'not equal', '!=', True, '==', 'equal to' ) # noqa: E241, E202, E501, E221 

34 GT = ('gt', 'greater', '>', False, '<=', 'not greater than') # noqa: E241, E202, E501, E221 

35 GTE = ('gte', 'greater or equal', '>=', False, '<', 'less than' ) # noqa: E241, E202, E501, E221 

36 LT = ('lt', 'lesser', '<', False, '>=', 'not less than' ) # noqa: E241, E202, E501, E221 

37 LTE = ('lte', 'lesser or equal', '<=', False, '>', 'greater than' ) # noqa: E241, E202, E501, E221 

38 SIMILAR = ('similar', 'similar', '~', False, '!~', 'not similar to' ) # noqa: E241, E202, E501, E221 

39 NOT_SIMILAR = ('similar', 'not similar', '!~', True, '~', 'similar to' ) # noqa: E241, E202, E501, E221 

40 IN = ('in_', 'in', 'in', False, 'not in', 'not in' ) # noqa: E241, E202, E501, E221 

41 NOT_IN = ('in_', 'not in', 'not in', True, 'in', 'in' ) # noqa: E241, E202, E501, E221 

42 INSTANCE_OF = ('instance_of', 'instance of', 'isinstance', False, 'not isinstance', 'not instance of' ) # noqa: E241, E202, E501, E221 

43 NOT_INSTANCE_OF = ('instance_of', 'not instance of', 'not isinstance', True, 'isinstance', 'instance of' ) # noqa: E241, E202, E501, E221 

44 

45 def __init__( 

46 self, function, text, symbol, negation, negation_symbol, message 

47 ): 

48 # type: (str, str, str, bool, str, str) -> None 

49 ''' 

50 Constructs Comparator instance. 

51 

52 Args: 

53 function (str): Enforce function name. 

54 text (str): Comparator as text. 

55 symbol (str): Comparator as symbol. 

56 negation (bool): Function is a negation. 

57 negation_symbol (str): Negated comparator as symbol. 

58 message (str): Error message fragment. 

59 ''' 

60 self.function = function 

61 self.text = text 

62 self.symbol = symbol 

63 self.negation = negation 

64 self.negation_symbol = negation_symbol 

65 self.message = message 

66 

67 @property 

68 def canonical(self): 

69 # type: () -> str 

70 ''' 

71 str: Canonical name of Comparator 

72 ''' 

73 return self.name.lower() 

74 

75 @staticmethod 

76 def from_string(string): 

77 # type: (str) -> Comparator 

78 ''' 

79 Constructs Comparator from given string. 

80 

81 Args: 

82 string (str): Comparator name. 

83 

84 Returns: 

85 Comparator: Comparator. 

86 ''' 

87 lut = {} 

88 for val in Comparator.__members__.values(): 

89 lut[val.text] = val 

90 lut[val.symbol] = val 

91 return lut[string.lower()] 

92 

93 

94class EnforceError(Exception): 

95 ''' 

96 Enforce error class. 

97 ''' 

98 pass 

99# ------------------------------------------------------------------------------ 

100 

101 

102class Enforce: 

103 ''' 

104 Faciltates inline testing. Super class for Enforcer subclasses. 

105 

106 Example: 

107 

108 >>> class Foo: 

109 def __init__(self, value): 

110 self.value = value 

111 def __repr__(self): 

112 return '<Foo>' 

113 >>> class Bar: 

114 def __init__(self, value): 

115 self.value = value 

116 def __repr__(self): 

117 return '<Bar>' 

118 

119 >>> Enforce(Foo(1), '==', Foo(2), 'type_name') 

120 >>> Enforce(Foo(1), '==', Bar(2), 'type_name') 

121 EnforceError: type_name of <Foo> is not equal to type_name of <Bar>. \ 

122Foo != Bar. 

123 

124 >>> class EnforceFooBar(Enforce): 

125 def get_value(self, item): 

126 return item.value 

127 >>> EnforceFooBar(Foo(1), '==', Bar(1), 'value') 

128 >>> EnforceFooBar(Foo(1), '==', Bar(2), 'value') 

129 EnforceError: value of <Foo> is not equal to value of <Bar>. 1 != 2. 

130 >>> EnforceFooBar(Foo(1), '~', Bar(5), 'value', epsilon=2) 

131 EnforceError: value of <Foo> is not similar to value of <Bar>. Delta 4 \ 

132is greater than epsilon 2. 

133 

134 >>> msg = '{a} is not like {b}. Please adjust your epsilon,: {epsilon}, ' 

135 >>> msg += 'to be higher than {delta}. ' 

136 >>> msg += 'A value: {a_val}. B value: {b_val}.' 

137 >>> EnforceFooBar(Foo(1), '~', Bar(5), 'value', epsilon=2, message=msg) 

138 <Foo> is not like <Bar>. Please adjust your epsilon: 2, to be higher \ 

139than 4. A value: 1. B value: 5. 

140 ''' 

141 def __init__( 

142 self, 

143 a, 

144 comparator, 

145 b, 

146 attribute=None, 

147 message=None, 

148 epsilon=0.01 

149 ): 

150 # type: (Any, str, Any, Optional[str], Optional[str], float) -> None 

151 ''' 

152 Validates predicate specified in constructor. 

153 

154 Args: 

155 a (object): First object to be tested. 

156 comparator (str): String representation of Comparator. 

157 b (object): Second object. 

158 attribute (str, optional): Attribute name of a and b. Default: None. 

159 message (str, optional): Custom error message. Default: None. 

160 epsilon (float, optional): Error threshold for a/b difference. 

161 Default: 0.01. 

162 

163 Raises: 

164 EnforceError: If predicate fails. 

165 

166 Returns: 

167 Enforce: Enforce instance. 

168 ''' 

169 # resolve everything 

170 comp = Comparator.from_string(comparator) # type: Comparator 

171 func = getattr(self, comp.function) 

172 a_val = a 

173 b_val = b 

174 if attribute is not None: 

175 getter = getattr(self, 'get_' + attribute) 

176 a_val = getter(a) 

177 if comp in [comp.IN, comp.NOT_IN]: 

178 b_val = [getter(x) for x in b] 

179 else: 

180 b_val = getter(b) 

181 

182 # get delta 

183 flag = comp.function == 'similar' 

184 delta = None 

185 if flag: 

186 delta = self.difference(a_val, b_val) 

187 

188 # create error message 

189 if message is None: 

190 message = self._get_message(attribute, comp) 

191 message = message.format( 

192 comparator=comp, 

193 a=a, 

194 b=b, 

195 a_val=a_val, 

196 b_val=b_val, 

197 attribute=attribute, 

198 delta=delta, 

199 epsilon=epsilon, 

200 ) 

201 

202 if flag: 

203 a_val = delta 

204 b_val = epsilon 

205 

206 # test a and b with func 

207 result = func(a_val, b_val) 

208 if comp.negation: 

209 result = not result 

210 if result is False: 

211 raise EnforceError(message) 

212 

213 def _get_message(self, attribute, comparator): 

214 # type: (Optional[str], Comparator) -> str 

215 ''' 

216 Creates an unformatted error message given an attribute name and 

217 comparator. 

218 

219 Args: 

220 attribute (str or None): Attribute name. 

221 comparator (Comparator): Comparator instance. 

222 

223 Returns: 

224 str: Error message. 

225 ''' 

226 message = '{a} is {comparator.message} {b}.' 

227 if attribute is not None: 

228 message = '{attribute} of {a} is {comparator.message} {attribute} of {b}.' 

229 

230 skip = [ 

231 Comparator.IN, 

232 Comparator.NOT_IN, 

233 Comparator.INSTANCE_OF, 

234 Comparator.NOT_INSTANCE_OF, 

235 ] 

236 if comparator in skip: 

237 pass 

238 elif comparator is Comparator.SIMILAR: 

239 message += ' Delta {delta} is greater than epsilon {epsilon}.' 

240 elif comparator is Comparator.NOT_SIMILAR: 

241 message += ' Delta {delta} is not greater than epsilon {epsilon}.' 

242 else: 

243 message += ' {a_val} {comparator.negation_symbol} {b_val}.' 

244 

245 return message 

246 

247 # COMPARATORS--------------------------------------------------------------- 

248 def eq(self, a, b): 

249 # type: (Any, Any) -> bool 

250 ''' 

251 Determines if a and b are equal. 

252 

253 Args: 

254 a (object): First object. 

255 b (object): Second object. 

256 

257 Returns: 

258 bool: True if a equals b. 

259 ''' 

260 return a == b 

261 

262 def gt(self, a, b): 

263 # type: (Any, Any) -> bool 

264 ''' 

265 Determines if a is greater than b. 

266 

267 Args: 

268 a (object): First object. 

269 b (object): Second object. 

270 

271 Returns: 

272 bool: True if a is greater than b. 

273 ''' 

274 return a > b 

275 

276 def gte(self, a, b): 

277 # type: (Any, Any) -> bool 

278 ''' 

279 Determines if a is greater than or equal to b. 

280 

281 Args: 

282 a (object): First object. 

283 b (object): Second object. 

284 

285 Returns: 

286 bool: True if a is greater than or equal to b. 

287 ''' 

288 return a >= b 

289 

290 def lt(self, a, b): 

291 # type: (Any, Any) -> bool 

292 ''' 

293 Determines if a is lesser than b. 

294 

295 Args: 

296 a (object): First object. 

297 b (object): Second object. 

298 

299 Returns: 

300 bool: True if a is lesser than b. 

301 ''' 

302 return a < b 

303 

304 def lte(self, a, b): 

305 # type: (Any, Any) -> bool 

306 ''' 

307 Determines if a is lesser than or equal to b. 

308 

309 Args: 

310 a (object): First object. 

311 b (object): Second object. 

312 

313 Returns: 

314 bool: True if a is lesser than or equal to b. 

315 ''' 

316 return a <= b 

317 

318 def similar(self, difference, epsilon=0.01): 

319 # type: (Union[int, float], float) -> bool 

320 ''' 

321 Determines if a/b difference given error threshold episilon. 

322 

323 Args: 

324 difference (int or float): Difference between a and b. 

325 epsilon (float, optional): Error threshold. Default: 0.01. 

326 

327 Returns: 

328 bool: True if difference is less than epsilon. 

329 ''' 

330 return difference < epsilon 

331 

332 def in_(self, a, b): 

333 # type: (Any, Union[List, Set, Tuple]) -> bool 

334 ''' 

335 Determines if a is in b. 

336 

337 Args: 

338 a (object): Member object. 

339 b (list or set or tuple): Container object. 

340 

341 Returns: 

342 bool: True if a is in b. 

343 ''' 

344 return a in b 

345 

346 def instance_of(self, a, b): 

347 # type: (Any, Any) -> bool 

348 ''' 

349 Determines if a is instance of b. 

350 

351 Args: 

352 a (type or list[type]): Instance object. 

353 b (object): Class object. 

354 

355 Returns: 

356 bool: True if a is instance of b. 

357 ''' 

358 if not isinstance(b, (tuple, list)): 

359 b = [b] 

360 if isinstance(b, list): 

361 b = tuple(b) 

362 return isinstance(a, b) 

363 

364 def difference(self, a, b): 

365 # type: (Any, Any) -> float 

366 ''' 

367 Calculates difference between a and b. 

368 

369 Args: 

370 a (object): First object. 

371 b (object): Second object. 

372 

373 Returns: 

374 float: Difference between a and b. 

375 ''' 

376 return abs(a - b) 

377 

378 # ATTRIBUTE-GETTERS--------------------------------------------------------- 

379 def get_type_name(self, item): 

380 # type: (Any) -> str 

381 ''' 

382 Gets __class__.__name__ of given item. 

383 

384 Args: 

385 item (object): Item. 

386 

387 Returns: 

388 str: item.__class__.__name__ 

389 ''' 

390 return item.__class__.__name__