userver
C++ Async Framework
Toggle main menu visibility
Loading...
Searching...
No Matches
matching.py
1
import
collections.abc
2
import
datetime
3
import
itertools
4
import
operator
5
import
re
6
import
typing
7
8
import
dateutil.parser
9
10
11
class
BaseError
(Exception):
12
pass
13
14
15
class
NoValueCapturedError
(
BaseError
):
16
pass
17
18
19
_Sentinel = object()
20
21
22
class
Any
:
23
"""Matches any value."""
24
25
def
__repr__(self):
26
return
'<Any>'
27
28
def
__eq__(self, other):
29
return
True
30
31
32
class
AnyString
:
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
46
class
RegexString
:
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
73
class
UuidString
(
RegexString
):
74
"""Matches lower-case hexadecimal uuid string."""
75
76
def
__init__(self):
77
super().__init__(
'^[0-9a-f]{32}$'
)
78
79
80
class
ObjectIdString
(
RegexString
):
81
"""Matches lower-case hexadecimal objectid string."""
82
83
def
__init__(self):
84
super().__init__(
'^[0-9a-f]{24}$'
)
85
86
87
class
DatetimeString
:
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
105
class
IsInstance
:
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
139
class
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
167
class
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
195
class
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
221
class
Comparator
:
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
242
class
Gt
(
Comparator
):
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
256
class
Ge
(
Comparator
):
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
270
class
Lt
(
Comparator
):
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
284
class
Le
(
Comparator
):
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
298
class
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
356
class
UnorderedList
:
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
410
class
AnyList
:
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
432
class
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
468
class
AnyDict
:
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
490
class
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
540
class
Capture
:
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
595
def
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
616
class
_ObjectTransform
:
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
634
def
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
667
def
_resolve_types(value):
668
return
getattr(value,
'__testsuite_types__'
, ())
669
670
671
def
_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
677
any_value =
Any
()
678
any_float =
IsInstance
(float)
679
any_integer =
IsInstance
(int)
680
any_numeric =
IsInstance
((int, float))
681
any_datetime =
IsInstance
(datetime.datetime)
682
any_timedelta =
IsInstance
(datetime.timedelta)
683
positive_float =
And
(any_float,
Gt
(0))
684
positive_integer =
And
(any_integer,
Gt
(0))
685
positive_numeric =
And
(any_numeric,
Gt
(0))
686
negative_float =
And
(any_float,
Lt
(0))
687
negative_integer =
And
(any_integer,
Lt
(0))
688
negative_numeric =
And
(any_numeric,
Lt
(0))
689
non_negative_float =
And
(any_float,
Ge
(0))
690
non_negative_integer =
And
(any_integer,
Ge
(0))
691
non_negative_numeric =
And
(any_numeric,
Ge
(0))
692
any_string =
AnyString
()
693
datetime_string =
DatetimeString
()
694
objectid_string =
ObjectIdString
()
695
uuid_string =
UuidString
()
696
697
any_dict =
AnyDict
()
698
any_list =
AnyList
()
en
testsuite
matching.py
Generated on
for userver by
Doxygen
1.17.0