userver: en/testsuite/matching.py Source File
Loading...
Searching...
No Matches
matching.py
2import datetime
3import itertools
4import operator
5import re
6import typing
7
8import dateutil.parser
9
10
11class BaseError(Exception):
12 pass
13
14
16 pass
17
18
19_Sentinel = object()
20
21
22class Any:
23 """Matches any value."""
24
25 def __repr__(self):
26 return '<Any>'
27
28 def __eq__(self, other):
29 return True
30
31
33 """Matches any string."""
34
35 __testsuite_types__ = (str,)
36
37 def __repr__(self):
38 return '<AnyString>'
39
40 def __eq__(self, other):
41 if isinstance(other, str):
42 return True
43 return any(issubclass(type, str) for type in _resolve_types(other))
44
45
47 """Match string with regular expression.
48
49 .. code-block:: python
50
51 assert response.json() == {
52 'order_id': matching.RegexString('^[0-9a-f]*$'),
53 ...
54 }
55 """
56
57 __testsuite_types__ = (str,)
58
59 def __init__(self, pattern):
60 self._pattern = re.compile(pattern)
61
62 def __repr__(self):
63 return f'<{self.__class__.__name__} pattern={self._pattern!r}>'
64
65 def __eq__(self, other):
66 if isinstance(other, str):
67 return self._pattern.match(other) is not None
68 if isinstance(other, RegexString):
69 return other._pattern == self._pattern
70 return False
71
72
74 """Matches lower-case hexadecimal uuid string."""
75
76 def __init__(self):
77 super().__init__('^[0-9a-f]{32}$')
78
79
81 """Matches lower-case hexadecimal objectid string."""
82
83 def __init__(self):
84 super().__init__('^[0-9a-f]{24}$')
85
86
88 """Matches datetime string in any format."""
89
90 __testsuite_types__ = (str,)
91
92 def __repr__(self):
93 return '<DatetimeString>'
94
95 def __eq__(self, other):
96 if isinstance(other, str):
97 try:
98 dateutil.parser.parse(other)
99 return True
100 except ValueError:
101 return False
102 return isinstance(other, DatetimeString)
103
104
106 """Match value by its type.
107
108 Use this class when you only need to check value type.
109
110 .. code-block:: python
111
112 assert response.json() == {
113 # order_id must be a string
114 'order_id': matching.IsInstance(str),
115 # int or float is acceptable here
116 'weight': matching.IsInstance([int, float]),
117 ...
118 }
119 """
120
121 def __init__(self, types):
122 self._types = types
123
124 def __repr__(self):
125 if isinstance(self._types, (list, tuple)):
126 type_names = [t.__name__ for t in self._types]
127 else:
128 type_names = [self._types.__name__]
129 return f'<IsInstance {", ".join(type_names)}>'
130
131 def __eq__(self, other):
132 if isinstance(other, self._types):
133 return True
134 if isinstance(other, IsInstance):
135 return self._types == other._types
136 return False
137
138
139class And:
140 """Logical AND on conditions.
141
142 .. code-block:: python
143
144 # match integer is in range [10, 100)
145 assert num == matching.And([matching.Ge(10), matching.Lt(100)])
146 """
147
148 def __init__(self, *conditions):
149 self._conditions = conditions
150
151 def __repr__(self):
152 conditions = [repr(cond) for cond in self._conditions]
153 return f'<And {", ".join(conditions)}>'
154
155 def __eq__(self, other):
156 if isinstance(other, And):
157 return self._conditions == other._conditions
158 for condition in self._conditions:
159 if condition != other:
160 return False
161 return True
162
163 def __testsuite_visit__(self, visit):
164 return And(*[visit(condition) for condition in self._conditions])
165
166
167class Or:
168 """Logical OR on conditions.
169
170 .. code-block:: python
171
172 # match integers abs(num) >= 10
173 assert num == matching.Or([matching.Ge(10), matching.Le(-10)])
174 """
175
176 def __init__(self, *conditions):
177 self._conditions = conditions
178
179 def __repr__(self):
180 conditions = [repr(cond) for cond in self._conditions]
181 return f'<Or {", ".join(conditions)}>'
182
183 def __eq__(self, other):
184 if isinstance(other, Or):
185 return self._conditions == other._conditions
186 for condition in self._conditions:
187 if condition == other:
188 return True
189 return False
190
191 def __testsuite_visit__(self, visit):
192 return Or(*[visit(condition) for condition in self._conditions])
193
194
195class Not:
196 """Condition inversion.
197
198 Example:
199
200 .. code-block:: python
201
202 # check value is not 1
203 assert value == matching.Not(1)
204 """
205
206 def __init__(self, condition):
207 self._condition = condition
208
209 def __repr__(self):
210 return f'<Not {self._condition!r}>'
211
212 def __eq__(self, other):
213 if isinstance(other, Not):
214 return self._condition == other._condition
215 return self._condition != other
216
217 def __testsuite_visit__(self, visit):
218 return Not(visit(self._condition))
219
220
222 op: typing.Callable[[typing.Any, typing.Any], bool] = operator.eq
223
224 def __init__(self, value):
225 self._value = value
226
227 def __repr__(self):
228 return f'<{self.op.__name__} {self._value}>'
229
230 def __eq__(self, other):
231 if isinstance(other, Comparator):
232 return self.op == other.op and self._value == other._value
233 try:
234 return self.op(other, self._value)
235 except TypeError:
236 return False
237
238 def __testsuite_visit__(self, visit):
239 return self.__class__(visit(self._value))
240
241
243 """Value is greater than.
244
245 Example:
246
247 .. code-block:: python
248
249 # Value must be > 10
250 assert value == matching.Gt(10)
251 """
252
253 op = operator.gt
254
255
257 """Value is greater or equal.
258
259 Example:
260
261 .. code-block:: python
262
263 # Value must be >= 10
264 assert value == matching.Ge(10)
265 """
266
267 op = operator.ge
268
269
271 """Value is less than.
272
273 Example:
274
275 .. code-block:: python
276
277 # Value must be < 10
278 assert value == matching.Lt(10)
279 """
280
281 op = operator.lt
282
283
285 """Value is less or equal.
286
287 Example:
288
289 .. code-block:: python
290
291 # Value must be <= 10
292 assert value == matching.Le(10)
293 """
294
295 op = operator.le
296
297
298class PartialDict(collections.abc.Mapping):
299 """Partial dictionary matching.
300
301 It might be useful to only check specific keys of a dictionary.
302 :py:class:`PartialDict` serves to solve this task.
303
304 :py:class:`PartialDict` is wrapper around regular `dict()` when instantiated
305 all arguments are passed as is to internal dict object.
306
307 Example:
308
309 .. code-block:: python
310
311 assert {'foo': 1, 'bar': 2} == matching.PartialDict({
312 # Only check for foo >= 1 ignoring other keys
313 'foo': matching.Ge(1),
314 })
315 """
316
317 __testsuite_types__ = (dict,)
318
319 def __init__(self, *args, **kwargs):
320 self._dict = dict(*args, **kwargs)
321
322 def __contains__(self, item):
323 return True
324
325 def __getitem__(self, item):
326 return self._dict.get(item, any_value)
327
328 def __iter__(self):
329 return iter(self._dict)
330
331 def __len__(self):
332 return len(self._dict)
333
334 def __repr__(self):
335 return f'<PartialDict {self._dict!r}>'
336
337 def __eq__(self, other):
338 if not isinstance(other, collections.abc.Mapping):
339 return False
340
341 for key in self:
342 if other.get(key) != self.get(key):
343 return False
344
345 return True
346
347 def __testsuite_visit__(self, visit):
348 return PartialDict(visit(self._dict))
349
350 def __testsuite_resolve_value__(self, other, report_error):
351 if not isinstance(other, collections.abc.Mapping):
352 return self
353 return {**other, **self._dict}
354
355
357 def __init__(self, sequence, key):
358 self._value = sorted(sequence, key=key)
359 self._key = key
360
361 def __repr__(self):
362 return f'<UnorderedList: {self._value}>'
363
364 def __eq__(self, other):
365 if isinstance(other, list):
366 return sorted(other, key=self._key) == self._value
367 if isinstance(other, UnorderedList):
368 return self._value == other._value and self._key == other._key
369 return False
370
371 def __testsuite_visit__(self, visit):
372 return UnorderedList(visit(self._value), self._key)
373
374 def __testsuite_resolve_value__(self, other, report_error):
375 if not isinstance(other, list):
376 return self
377
378 sort_key = self._key or (lambda x: x)
379 other_sorted = sorted(
380 enumerate(other), key=lambda x: (sort_key(x[1]), x[0])
381 )
382
383 idx_seq = itertools.count(len(other_sorted))
384 it_self = iter(self._value)
385
386 def doit():
387 item_self = next(it_self, _Sentinel)
388 for idx_other, item_other in other_sorted:
389 if item_self is _Sentinel:
390 return
391 while sort_key(item_other) > sort_key(item_self):
392 yield next(idx_seq), item_self
393 item_self = next(it_self, _Sentinel)
394 if item_self is _Sentinel:
395 return
396 if sort_key(item_other) < sort_key(item_self):
397 continue
398 if item_other == item_self:
399 yield idx_other, item_other
400 else:
401 yield next(idx_seq), item_self
402 item_self = next(it_self, _Sentinel)
403 if item_self is not _Sentinel:
404 yield next(idx_seq), item_self
405 yield from zip(idx_seq, it_self)
406
407 return [item for _, item in sorted(doit(), key=operator.itemgetter(0))]
408
409
411 """Value is a list.
412
413 Example:
414
415 .. code-block:: python
416
417 assert ['foo', 'bar'] == matching.any_list
418 """
419
420 def __repr__(self):
421 return '<AnyList>'
422
423 def __eq__(self, other):
424 return isinstance(other, (list, AnyList))
425
426 def __testsuite_resolve_value__(self, other, report_error):
427 if not isinstance(other, list):
428 return self
429 return other
430
431
432class ListOf:
433 """Value is a list of values.
434
435 Example:
436
437 .. code-block:: python
438
439 assert ['foo', 'bar'] == matching.ListOf(matching.any_string)
440 assert [1, 2] != matching.ListOf(matching.any_string)
441 """
442
443 def __init__(self, value=Any()):
444 self._value = value
445
446 def __repr__(self):
447 return f'<ListOf value={self._value}>'
448
449 def __eq__(self, other):
450 if isinstance(other, list):
451 for value in other:
452 if self._value != value:
453 return False
454 return True
455 if isinstance(other, ListOf):
456 return self._value == other._value
457 return False
458
459 def __testsuite_visit__(self, visit):
460 return ListOf(visit(self._value))
461
462 def __testsuite_resolve_value__(self, other, report_error):
463 if not isinstance(other, list):
464 return self
465 return [self._value] * len(other)
466
467
469 """Value is a dictionary.
470
471 Example:
472
473 .. code-block:: python
474
475 assert {'foo': 'bar'} == matching.any_dict
476 """
477
478 def __repr__(self):
479 return '<AnyDict>'
480
481 def __eq__(self, other):
482 return isinstance(other, (dict, AnyDict))
483
484 def __testsuite_resolve_value__(self, other, report_error):
485 if not isinstance(other, collections.abc.Mapping):
486 return self
487 return other
488
489
490class DictOf:
491 """Value is a dictionary of (key, value) pairs.
492
493 Example:
494
495 .. code-block:: python
496
497 pred = matching.DictOf(key=matching.any_string, value=matching.any_string)
498 assert pred == {'foo': 'bar'}
499 assert pred != {'foo': 1}
500 assert pred != {1: 'bar'}
501 """
502
503 def __init__(self, key=Any(), value=Any()):
504 self._key = key
505 self._value = value
506
507 def __repr__(self):
508 return f'<DictOf key={self._key} value={self._value}>'
509
510 def __eq__(self, other):
511 if isinstance(other, dict):
512 for key, value in other.items():
513 if self._key != key:
514 return False
515 if self._value != value:
516 return False
517 return True
518 if isinstance(other, DictOf):
519 return self._key == other._key and self._value == other._value
520 return False
521
522 def __testsuite_visit__(self, visit):
523 return DictOf(visit(self._key), visit(self._value))
524
525 def __testsuite_resolve_value__(self, other, report_error):
526 if not isinstance(other, collections.abc.Mapping):
527 return self
528
529 result = {}
530 for key, value in other.items():
531 if key != self._key:
532 report_error(
533 f'dict key must match {self._key} expression',
534 path=f'[{key!r}]',
535 )
536 result[key] = self._value
537 return result
538
539
541 """Capture matched value(s).
542
543 Example:
544
545 .. code-block:: python
546
547 # You can define matching rule out of pattern
548 capture_foo = matching.Capture(matching.any_string)
549 pattern = {'foo': capture_foo}
550 assert pattern == {'foo': 'bar'}
551 assert capture_foo.value == 'bar'
552 assert capture_foo.values_list == ['bar']
553
554 # Or do it later
555 capture_foo = matching.Capture()
556 pattern = {'foo': capture_foo(matching.any_string)}
557 assert pattern == {'foo': 'bar'}
558 assert capture_foo.value == 'bar'
559 assert capture_foo.values_list == ['bar']
560 """
561
562 def __init__(self, value=Any(), _link_captured=None):
563 self._value = value
564 if _link_captured is None:
565 self._captured = []
566 else:
567 self._captured = _link_captured
568
569 @property
570 def value(self):
571 if self._captured:
572 return self._captured[0]
573 raise NoValueCapturedError(f'No value captured for value {self._value}')
574
575 @property
576 def values_list(self):
577 return self._captured
578
579 def __eq__(self, other):
580 if self._value != other:
581 return False
582 self._captured.append(other)
583 return True
584
585 def __call__(self, value):
586 return Capture(value, _link_captured=self._captured)
587
588 def __testsuite_visit__(self, visit):
589 return Capture(visit(self._value), self._captured)
590
591 def __testsuite_resolve_value__(self, other, report_error):
592 return _resolve_value(self._value, other, report_error)
593
594
595def unordered_list(sequence, *, key=None):
596 """Unordered list comparison.
597
598 You may want to compare lists without respect to order. For instance,
599 when your service is serializing std::unordered_map to array.
600
601 `unordered_list` can help you with that. It sorts both array before
602 comparison.
603
604 :param sequence: Initial sequence
605 :param key: Sorting key function
606
607 Example:
608
609 .. code-block:: python
610
611 assert [3, 2, 1] == matching.unordered_list([1, 2, 3])
612 """
613 return UnorderedList(sequence, key)
614
615
617 def visit(self, value):
618 if isinstance(value, dict):
619 return self.visit_dict(value)
620 if isinstance(value, list):
621 return self.visit_list(value)
622 visit = getattr(value, '__testsuite_visit__', None)
623 if visit:
624 return visit(self.visit)
625 return value
626
627 def visit_dict(self, value):
628 return {key: self.visit(value) for key, value in value.items()}
629
630 def visit_list(self, value):
631 return [self.visit(item) for item in value]
632
633
634def recursive_partial_dict(*args, **kwargs):
635 """Creates recursive partial dict.
636
637 Traverse input dict and create `PartialDict` for nested dicts.
638 Supports visiting `testsuite.matching` predicates. Skips inner
639 :py:class:`PartialDict` nodes in order to allow user to customize
640 behavior.
641
642 l Example:
643
644 .. code-block:: python
645
646 assert {
647 'foo': {'bar': 123, 'extra'}, 'extra'
648 } == matching.recursive_partial_dict({
649 'foo: {'bar': 123}
650 })
651 """
652
653 class Transform(_ObjectTransform):
654 def visit(self, value):
655 if isinstance(value, PartialDict):
656 return value
657 return super().visit(value)
658
659 def visit_dict(self, value):
660 value = super().visit_dict(value)
661 return PartialDict(value)
662
663 root = dict(*args, **kwargs)
664 return Transform().visit(root)
665
666
667def _resolve_types(value):
668 return getattr(value, '__testsuite_types__', ())
669
670
671def _resolve_value(obj, other, report_error):
672 if hasattr(obj, '__testsuite_resolve_value__'):
673 return obj.__testsuite_resolve_value__(other, report_error)
674 return obj
675
676
677any_value = Any()
678any_float = IsInstance(float)
679any_integer = IsInstance(int)
680any_numeric = IsInstance((int, float))
681any_datetime = IsInstance(datetime.datetime)
682any_timedelta = IsInstance(datetime.timedelta)
683positive_float = And(any_float, Gt(0))
684positive_integer = And(any_integer, Gt(0))
685positive_numeric = And(any_numeric, Gt(0))
686negative_float = And(any_float, Lt(0))
687negative_integer = And(any_integer, Lt(0))
688negative_numeric = And(any_numeric, Lt(0))
689non_negative_float = And(any_float, Ge(0))
690non_negative_integer = And(any_integer, Ge(0))
691non_negative_numeric = And(any_numeric, Ge(0))
692any_string = AnyString()
693datetime_string = DatetimeString()
694objectid_string = ObjectIdString()
695uuid_string = UuidString()
696
697any_dict = AnyDict()
698any_list = AnyList()