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
« 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
3from enum import Enum
4# ------------------------------------------------------------------------------
7'''
8The enforce module contains the Enforce class which is used for faciltating
9inline testing, inside of function defitions and test definitions.
10'''
13class Comparator(Enum):
14 '''
15 Enum for comparison operators used by Enforce.
17 Includes:
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
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.
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
67 @property
68 def canonical(self):
69 # type: () -> str
70 '''
71 str: Canonical name of Comparator
72 '''
73 return self.name.lower()
75 @staticmethod
76 def from_string(string):
77 # type: (str) -> Comparator
78 '''
79 Constructs Comparator from given string.
81 Args:
82 string (str): Comparator name.
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()]
94class EnforceError(Exception):
95 '''
96 Enforce error class.
97 '''
98 pass
99# ------------------------------------------------------------------------------
102class Enforce:
103 '''
104 Faciltates inline testing. Super class for Enforcer subclasses.
106 Example:
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>'
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.
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.
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.
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.
163 Raises:
164 EnforceError: If predicate fails.
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)
182 # get delta
183 flag = comp.function == 'similar'
184 delta = None
185 if flag:
186 delta = self.difference(a_val, b_val)
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 )
202 if flag:
203 a_val = delta
204 b_val = epsilon
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)
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.
219 Args:
220 attribute (str or None): Attribute name.
221 comparator (Comparator): Comparator instance.
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}.'
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}.'
245 return message
247 # COMPARATORS---------------------------------------------------------------
248 def eq(self, a, b):
249 # type: (Any, Any) -> bool
250 '''
251 Determines if a and b are equal.
253 Args:
254 a (object): First object.
255 b (object): Second object.
257 Returns:
258 bool: True if a equals b.
259 '''
260 return a == b
262 def gt(self, a, b):
263 # type: (Any, Any) -> bool
264 '''
265 Determines if a is greater than b.
267 Args:
268 a (object): First object.
269 b (object): Second object.
271 Returns:
272 bool: True if a is greater than b.
273 '''
274 return a > b
276 def gte(self, a, b):
277 # type: (Any, Any) -> bool
278 '''
279 Determines if a is greater than or equal to b.
281 Args:
282 a (object): First object.
283 b (object): Second object.
285 Returns:
286 bool: True if a is greater than or equal to b.
287 '''
288 return a >= b
290 def lt(self, a, b):
291 # type: (Any, Any) -> bool
292 '''
293 Determines if a is lesser than b.
295 Args:
296 a (object): First object.
297 b (object): Second object.
299 Returns:
300 bool: True if a is lesser than b.
301 '''
302 return a < b
304 def lte(self, a, b):
305 # type: (Any, Any) -> bool
306 '''
307 Determines if a is lesser than or equal to b.
309 Args:
310 a (object): First object.
311 b (object): Second object.
313 Returns:
314 bool: True if a is lesser than or equal to b.
315 '''
316 return a <= b
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.
323 Args:
324 difference (int or float): Difference between a and b.
325 epsilon (float, optional): Error threshold. Default: 0.01.
327 Returns:
328 bool: True if difference is less than epsilon.
329 '''
330 return difference < epsilon
332 def in_(self, a, b):
333 # type: (Any, Union[List, Set, Tuple]) -> bool
334 '''
335 Determines if a is in b.
337 Args:
338 a (object): Member object.
339 b (list or set or tuple): Container object.
341 Returns:
342 bool: True if a is in b.
343 '''
344 return a in b
346 def instance_of(self, a, b):
347 # type: (Any, Any) -> bool
348 '''
349 Determines if a is instance of b.
351 Args:
352 a (type or list[type]): Instance object.
353 b (object): Class object.
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)
364 def difference(self, a, b):
365 # type: (Any, Any) -> float
366 '''
367 Calculates difference between a and b.
369 Args:
370 a (object): First object.
371 b (object): Second object.
373 Returns:
374 float: Difference between a and b.
375 '''
376 return abs(a - b)
378 # ATTRIBUTE-GETTERS---------------------------------------------------------
379 def get_type_name(self, item):
380 # type: (Any) -> str
381 '''
382 Gets __class__.__name__ of given item.
384 Args:
385 item (object): Item.
387 Returns:
388 str: item.__class__.__name__
389 '''
390 return item.__class__.__name__