assertpy 2.35.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- assertpy/__init__.py +87 -0
- assertpy/_clustering.py +511 -0
- assertpy/_dangling.py +505 -0
- assertpy/_engine/__init__.py +1 -0
- assertpy/_engine/_builder_check_typing.py +1365 -0
- assertpy/_engine/_capable_typing.py +772 -0
- assertpy/_engine/_check_typing.py +753 -0
- assertpy/_engine/_compare.py +983 -0
- assertpy/_engine/_compat.py +38 -0
- assertpy/_engine/_contract.py +1926 -0
- assertpy/_engine/_diff.py +1058 -0
- assertpy/_engine/_equality.py +843 -0
- assertpy/_engine/_introspection.py +729 -0
- assertpy/_engine/_membership.py +522 -0
- assertpy/_engine/_mixin_base.py +258 -0
- assertpy/_engine/_negated_typing.py +2176 -0
- assertpy/_engine/_operations.py +75 -0
- assertpy/_engine/_ordering.py +884 -0
- assertpy/_engine/_pairing.py +70 -0
- assertpy/_engine/_path.py +158 -0
- assertpy/_engine/_poll_typing.py +6039 -0
- assertpy/_engine/_require.py +312 -0
- assertpy/_engine/_size.py +31 -0
- assertpy/_engine/_text.py +39 -0
- assertpy/_engine/_typing.py +1226 -0
- assertpy/_hints.py +1213 -0
- assertpy/_inline.py +484 -0
- assertpy/_matcher_impls.py +1876 -0
- assertpy/_passes.py +249 -0
- assertpy/_renamed.py +64 -0
- assertpy/_satisfies.py +835 -0
- assertpy/_snapshot_codec.py +290 -0
- assertpy/assertpy.py +2640 -0
- assertpy/async_assertions.py +864 -0
- assertpy/base.py +862 -0
- assertpy/behave_matchers.py +96 -0
- assertpy/bytes_mixin.py +158 -0
- assertpy/collection.py +528 -0
- assertpy/contains.py +1068 -0
- assertpy/dataframe.py +200 -0
- assertpy/date.py +327 -0
- assertpy/dict.py +249 -0
- assertpy/dynamic.py +222 -0
- assertpy/errors.py +782 -0
- assertpy/exception.py +838 -0
- assertpy/extracting.py +266 -0
- assertpy/file.py +324 -0
- assertpy/helpers.py +747 -0
- assertpy/http_mixin.py +218 -0
- assertpy/json_mixin.py +1265 -0
- assertpy/matchers.py +639 -0
- assertpy/numeric.py +696 -0
- assertpy/outcome.py +163 -0
- assertpy/py.typed +0 -0
- assertpy/pytest_plugin.py +1650 -0
- assertpy/snapshot.py +1031 -0
- assertpy/string.py +803 -0
- assertpy/warning.py +137 -0
- assertpy-2.35.0.dist-info/METADATA +324 -0
- assertpy-2.35.0.dist-info/RECORD +63 -0
- assertpy-2.35.0.dist-info/WHEEL +4 -0
- assertpy-2.35.0.dist-info/entry_points.txt +2 -0
- assertpy-2.35.0.dist-info/licenses/LICENSE +28 -0
assertpy/matchers.py
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from typing import TYPE_CHECKING, Any, NamedTuple, SupportsIndex, TypeVar, cast, overload
|
|
6
|
+
|
|
7
|
+
from ._engine._introspection import is_same_implementation
|
|
8
|
+
from ._engine._require import argument, raised_on_purpose, refuse, verdict
|
|
9
|
+
from ._matcher_impls import (
|
|
10
|
+
AllOfMatcher,
|
|
11
|
+
AnyOfMatcher,
|
|
12
|
+
BetweenMatcher,
|
|
13
|
+
CloseToMatcher,
|
|
14
|
+
ContainsMatcher,
|
|
15
|
+
ContainsOnlyMatcher,
|
|
16
|
+
ContainsStringMatcher,
|
|
17
|
+
EachMatcher,
|
|
18
|
+
EndsWithMatcher,
|
|
19
|
+
EqualToMatcher,
|
|
20
|
+
GreaterThanMatcher,
|
|
21
|
+
GreaterThanOrEqualToMatcher,
|
|
22
|
+
HasLengthMatcher,
|
|
23
|
+
HasPropertyMatcher,
|
|
24
|
+
IgnoreMatcher,
|
|
25
|
+
IsAfterMatcher,
|
|
26
|
+
IsBeforeMatcher,
|
|
27
|
+
IsCallableMatcher,
|
|
28
|
+
IsDivisibleByMatcher,
|
|
29
|
+
IsEmptyMatcher,
|
|
30
|
+
IsEvenMatcher,
|
|
31
|
+
IsFalsyMatcher,
|
|
32
|
+
IsInMatcher,
|
|
33
|
+
IsNegativeMatcher,
|
|
34
|
+
IsNoneMatcher,
|
|
35
|
+
IsNonEmptyStringMatcher,
|
|
36
|
+
IsNotEmptyMatcher,
|
|
37
|
+
IsNotNoneMatcher,
|
|
38
|
+
IsNowMatcher,
|
|
39
|
+
IsOddMatcher,
|
|
40
|
+
IsPositiveMatcher,
|
|
41
|
+
IsSortedMatcher,
|
|
42
|
+
IsSubsetOfMatcher,
|
|
43
|
+
IsTruthyMatcher,
|
|
44
|
+
IsUuidMatcher,
|
|
45
|
+
IsZeroMatcher,
|
|
46
|
+
LessThanMatcher,
|
|
47
|
+
LessThanOrEqualToMatcher,
|
|
48
|
+
MatchesRegexMatcher,
|
|
49
|
+
NotMatcher,
|
|
50
|
+
StartsWithMatcher,
|
|
51
|
+
StructureMatcher,
|
|
52
|
+
_Composable,
|
|
53
|
+
_is_matcher,
|
|
54
|
+
_refused,
|
|
55
|
+
)
|
|
56
|
+
from ._matcher_impls import (
|
|
57
|
+
BaseMatcher as BaseMatcher,
|
|
58
|
+
)
|
|
59
|
+
from ._matcher_impls import (
|
|
60
|
+
IsInstanceOfMatcher as IsInstanceOfMatcher,
|
|
61
|
+
)
|
|
62
|
+
from ._matcher_impls import (
|
|
63
|
+
IsTypeOfMatcher as IsTypeOfMatcher,
|
|
64
|
+
)
|
|
65
|
+
from ._matcher_impls import (
|
|
66
|
+
Matcher as Matcher,
|
|
67
|
+
)
|
|
68
|
+
from ._matcher_impls import (
|
|
69
|
+
MatchResult as MatchResult,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if TYPE_CHECKING:
|
|
73
|
+
import re
|
|
74
|
+
from collections.abc import Callable, Iterable
|
|
75
|
+
|
|
76
|
+
# the element a membership matcher judges, so `match.contains("x")` is a matcher for collections of strings
|
|
77
|
+
_Item = TypeVar("_Item")
|
|
78
|
+
|
|
79
|
+
from ._matcher_impls import ClassInfo
|
|
80
|
+
|
|
81
|
+
__tracebackhide__ = raised_on_purpose
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class _Resolved(NamedTuple):
|
|
85
|
+
"""What one reading of the argument decided: how to ask it, and how to say what was asked."""
|
|
86
|
+
|
|
87
|
+
apply: Callable[[object], bool]
|
|
88
|
+
describe: Callable[[], str]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _resolved(matcher: Matcher[Any] | Callable[..., object]) -> _Resolved:
|
|
92
|
+
"""`_apply_matcher`'s dispatch, decided once for a matcher a loop is about to ask many times.
|
|
93
|
+
|
|
94
|
+
`_is_matcher` walks the protocol for anything that is not a `BaseMatcher`, which a loop over ten
|
|
95
|
+
thousand items pays per item: `any_satisfy` over ten thousand measured 13.05 ms against 0.59 ms.
|
|
96
|
+
|
|
97
|
+
Kept beside `_apply_matcher` rather than under it: routing the single-call form through a closure
|
|
98
|
+
measured 2.54 ms against 2.75 ms over two thousand matchers asked once each, which is what
|
|
99
|
+
`satisfies_exactly` does.
|
|
100
|
+
|
|
101
|
+
The rule this states for a whole loop, in the same order `_apply_matcher` reads it: the matcher
|
|
102
|
+
protocol first, a callable second, and that reading holds for every value the loop asks about. An
|
|
103
|
+
argument that grows or drops the three members while the loop runs keeps being asked the way it was
|
|
104
|
+
read at the start, and the description in the failure comes from that same reading rather than from
|
|
105
|
+
reading it again afterwards. Nothing here is an immutability claim: a function and a
|
|
106
|
+
`functools.partial` both take attributes, which is exactly why the reading is fixed rather than
|
|
107
|
+
repeated.
|
|
108
|
+
"""
|
|
109
|
+
if _is_matcher(matcher):
|
|
110
|
+
# bound here, not looked up per value: a matcher that replaces its own member mid-loop would
|
|
111
|
+
# otherwise be asked something other than what was read, and the lookup costs per item besides
|
|
112
|
+
matches = matcher.matches
|
|
113
|
+
return _Resolved(lambda value: bool(verdict(matches(value), subject="the matcher")), matcher.describe)
|
|
114
|
+
if callable(matcher):
|
|
115
|
+
predicate = cast("Callable[..., object]", matcher)
|
|
116
|
+
return _Resolved(lambda value: bool(verdict(predicate(value))), lambda: _describe_callable(predicate))
|
|
117
|
+
refuse(matcher, "a Matcher or a callable", subject=argument("matcher"))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _apply_matcher(matcher: Matcher[Any] | Callable[..., object], value: object) -> bool:
|
|
121
|
+
"""Evaluate a ``Matcher`` or one-arg callable against ``value``.
|
|
122
|
+
|
|
123
|
+
Shared resolution for an assertion that accepts either a `Matcher` or a callable predicate,
|
|
124
|
+
mirroring the dispatch in ``satisfies``/``each``: a `Matcher` is checked via ``matches``, a callable
|
|
125
|
+
via its return value. Its callers, ``satisfies_exactly`` and the snapshot placeholders, refuse
|
|
126
|
+
anything else before the value is read, so that a refusal does not wait on the data.
|
|
127
|
+
"""
|
|
128
|
+
if _is_matcher(matcher):
|
|
129
|
+
return bool(verdict(matcher.matches(value), subject="the matcher"))
|
|
130
|
+
return bool(verdict(cast("Callable[..., object]", matcher)(value)))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _evaluate_matcher(matcher: Matcher[Any], value: object) -> MatchResult:
|
|
134
|
+
"""One `MatchResult` from any matcher, including one that never heard of `evaluate()`.
|
|
135
|
+
|
|
136
|
+
`BaseMatcher` gets `evaluate()` from its base, but a matcher is allowed to be duck-typed: the
|
|
137
|
+
`Matcher` protocol is three methods and stays three, because widening it would un-match every
|
|
138
|
+
third-party matcher written against the documented shape. The composition that `BaseMatcher` does
|
|
139
|
+
for its subclasses is done here for everything else.
|
|
140
|
+
"""
|
|
141
|
+
evaluate = getattr(matcher, "evaluate", None)
|
|
142
|
+
if evaluate is not None:
|
|
143
|
+
return evaluate(value)
|
|
144
|
+
if verdict(matcher.matches(value), subject="the matcher"):
|
|
145
|
+
return MatchResult(matched=True, description=matcher.describe())
|
|
146
|
+
return _refused(matcher, value)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _describe_callable(predicate: Callable[..., object]) -> str:
|
|
150
|
+
"""A readable, deterministic name for a predicate callable in a message.
|
|
151
|
+
|
|
152
|
+
Avoids the ``<function <lambda> at 0x...>`` repr, whose address is noise that changes every run,
|
|
153
|
+
while keeping a named function's name (the useful part) and still flagging a lambda as a lambda.
|
|
154
|
+
"""
|
|
155
|
+
name = getattr(predicate, "__name__", None)
|
|
156
|
+
if name == "<lambda>":
|
|
157
|
+
return "a lambda predicate"
|
|
158
|
+
if name:
|
|
159
|
+
return f"predicate {name}()"
|
|
160
|
+
return "the given predicate"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _describe_matcher(matcher: Matcher[Any] | Callable[..., object]) -> str:
|
|
164
|
+
"""Describe a ``Matcher`` or callable for the "expected" half of an error or diff entry."""
|
|
165
|
+
if _is_matcher(matcher):
|
|
166
|
+
return matcher.describe()
|
|
167
|
+
return _describe_callable(cast("Callable[..., object]", matcher))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
_custom_matchers: dict[str, Callable[..., BaseMatcher]] = {}
|
|
171
|
+
_custom_matchers_lock = threading.Lock()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def register_matcher(
|
|
175
|
+
name: str, *, override: bool = False
|
|
176
|
+
) -> Callable[[Callable[..., BaseMatcher]], Callable[..., BaseMatcher]]:
|
|
177
|
+
"""Register a custom matcher factory on the ``match`` namespace.
|
|
178
|
+
|
|
179
|
+
A name already taken by another custom matcher is refused unless ``override`` says otherwise, so
|
|
180
|
+
two libraries registering ``has_status`` find out at import rather than by whichever imported
|
|
181
|
+
last quietly winning.
|
|
182
|
+
|
|
183
|
+
A name already taken by a *built-in* matcher is refused outright, and ``override`` cannot lift it.
|
|
184
|
+
``match.equal_to`` resolves through ordinary attribute lookup, which never reaches this registry,
|
|
185
|
+
so such a registration would not lose an argument - it would simply never run.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
name: the name to register on ``match`` (e.g. ``"is_valid_email"``)
|
|
189
|
+
override: replace an existing custom matcher of the same name instead of refusing
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
A decorator that registers the wrapped function and returns it unchanged.
|
|
193
|
+
|
|
194
|
+
Raises:
|
|
195
|
+
TypeError: if ``name`` is not a string, or the decorated object is not callable
|
|
196
|
+
ValueError: if ``name`` is not an identifier, names a built-in matcher, or names a custom one
|
|
197
|
+
already registered and ``override`` is false
|
|
198
|
+
|
|
199
|
+
Examples:
|
|
200
|
+
Register a simple matcher:
|
|
201
|
+
|
|
202
|
+
@register_matcher("is_valid_email")
|
|
203
|
+
def is_valid_email():
|
|
204
|
+
return match.matches_regex(r"^[\\w.-]+@[\\w.-]+\\.\\w+$")
|
|
205
|
+
|
|
206
|
+
assert_that(email).satisfies(match.is_valid_email())
|
|
207
|
+
|
|
208
|
+
Register a parametrised matcher:
|
|
209
|
+
|
|
210
|
+
@register_matcher("has_status")
|
|
211
|
+
def has_status(expected: str):
|
|
212
|
+
return match.has_property("status", match.equal_to(expected))
|
|
213
|
+
|
|
214
|
+
assert_that(order).satisfies(match.has_status("active"))
|
|
215
|
+
"""
|
|
216
|
+
if not isinstance(name, str):
|
|
217
|
+
refuse(name, "a string", subject=argument("name"))
|
|
218
|
+
if not name.isidentifier():
|
|
219
|
+
message = f"name must be a valid Python identifier, got {name!r}"
|
|
220
|
+
_assertpy_refusal = True
|
|
221
|
+
raise ValueError(message)
|
|
222
|
+
if hasattr(_MatchNamespace, name):
|
|
223
|
+
message = (
|
|
224
|
+
f"match.{name} is a built-in matcher, and attribute lookup reaches it before this "
|
|
225
|
+
f"registry: a custom {name!r} would never be called. Register it under another name."
|
|
226
|
+
)
|
|
227
|
+
_assertpy_refusal = True
|
|
228
|
+
raise ValueError(message)
|
|
229
|
+
|
|
230
|
+
def decorator(func: Callable[..., BaseMatcher]) -> Callable[..., BaseMatcher]:
|
|
231
|
+
if not callable(func):
|
|
232
|
+
refuse(func, "callable", subject=argument("func"))
|
|
233
|
+
with _custom_matchers_lock:
|
|
234
|
+
# a module imported twice, or a per-module fixture, is not two libraries fighting over one name
|
|
235
|
+
clash = name in _custom_matchers and not is_same_implementation(_custom_matchers[name], func)
|
|
236
|
+
if clash and not override:
|
|
237
|
+
message = (
|
|
238
|
+
f"a custom matcher named {name!r} is already registered; pass override=True to "
|
|
239
|
+
f"replace it, or unregister_matcher({name!r}) first"
|
|
240
|
+
)
|
|
241
|
+
_assertpy_refusal = True
|
|
242
|
+
raise ValueError(message)
|
|
243
|
+
_custom_matchers[name] = func
|
|
244
|
+
return func
|
|
245
|
+
|
|
246
|
+
return decorator
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def unregister_matcher(name: str) -> None:
|
|
250
|
+
"""Remove a previously registered custom matcher.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
name: the matcher name to remove
|
|
254
|
+
|
|
255
|
+
Raises:
|
|
256
|
+
KeyError: if the name is not registered
|
|
257
|
+
"""
|
|
258
|
+
with _custom_matchers_lock:
|
|
259
|
+
if name not in _custom_matchers:
|
|
260
|
+
message = f"no custom matcher registered with name {name!r}"
|
|
261
|
+
_assertpy_refusal = True
|
|
262
|
+
raise KeyError(message)
|
|
263
|
+
del _custom_matchers[name]
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def clear_custom_matchers() -> None:
|
|
267
|
+
"""Remove all registered custom matchers."""
|
|
268
|
+
with _custom_matchers_lock:
|
|
269
|
+
_custom_matchers.clear()
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class _MatchNamespace:
|
|
273
|
+
"""Factory namespace for creating matcher instances.
|
|
274
|
+
|
|
275
|
+
Usage:
|
|
276
|
+
|
|
277
|
+
from assertpy import match
|
|
278
|
+
|
|
279
|
+
assert_that(value).satisfies(match.greater_than(5) & match.less_than(10))
|
|
280
|
+
assert_that(items).each(match.is_positive())
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
@staticmethod
|
|
284
|
+
def equal_to(expected: object, strict_types: bool = False, **options: object) -> EqualToMatcher:
|
|
285
|
+
"""Matcher for a value equal to ``expected``.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
expected: the value to compare against
|
|
289
|
+
strict_types: also require the same type, so ``True`` no longer matches ``1``. The same
|
|
290
|
+
relation ``is_equal_to(..., strict_types=True)`` applies, spelled for a spec.
|
|
291
|
+
**options: the rest of what
|
|
292
|
+
[`is_equal_to()`][assertpy.base.BaseMixin.is_equal_to] accepts, with the same meaning:
|
|
293
|
+
``tolerance``, ``rel``, ``comparators``, ``ignore``, ``include`` and ``ignore_null``. One
|
|
294
|
+
relation, one set of knobs, whichever way it is spelled.
|
|
295
|
+
|
|
296
|
+
Examples:
|
|
297
|
+
Usage:
|
|
298
|
+
|
|
299
|
+
assert_that(reading).satisfies(match.equal_to(expected, tolerance=0.01))
|
|
300
|
+
assert_that(payload).matches_structure({"user": match.equal_to(user, ignore="updated_at")})
|
|
301
|
+
"""
|
|
302
|
+
return EqualToMatcher(expected, strict_types, **options)
|
|
303
|
+
|
|
304
|
+
@staticmethod
|
|
305
|
+
def greater_than(val: object) -> GreaterThanMatcher:
|
|
306
|
+
"""Matcher for a value greater than ``val``."""
|
|
307
|
+
return GreaterThanMatcher(val)
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def greater_than_or_equal_to(val: object) -> GreaterThanOrEqualToMatcher:
|
|
311
|
+
"""Matcher for a value greater than or equal to ``val``."""
|
|
312
|
+
return GreaterThanOrEqualToMatcher(val)
|
|
313
|
+
|
|
314
|
+
@staticmethod
|
|
315
|
+
def less_than(val: object) -> LessThanMatcher:
|
|
316
|
+
"""Matcher for a value less than ``val``."""
|
|
317
|
+
return LessThanMatcher(val)
|
|
318
|
+
|
|
319
|
+
@staticmethod
|
|
320
|
+
def less_than_or_equal_to(val: object) -> LessThanOrEqualToMatcher:
|
|
321
|
+
"""Matcher for a value less than or equal to ``val``."""
|
|
322
|
+
return LessThanOrEqualToMatcher(val)
|
|
323
|
+
|
|
324
|
+
@staticmethod
|
|
325
|
+
def between(low: object, high: object) -> BetweenMatcher:
|
|
326
|
+
"""Matcher for a value in the inclusive range ``low`` to ``high``."""
|
|
327
|
+
return BetweenMatcher(low, high)
|
|
328
|
+
|
|
329
|
+
@staticmethod
|
|
330
|
+
def close_to(expected: object, tolerance: object = None, *, rel: object = None) -> CloseToMatcher:
|
|
331
|
+
"""Matcher for a value within ``tolerance`` of ``expected``, measured as `is_close_to()` measures it.
|
|
332
|
+
|
|
333
|
+
The distance is taken three ways, the difference and the window around each side, and any of them
|
|
334
|
+
holding is enough, which a float rounds differently at the boundary. An infinity is close only to
|
|
335
|
+
itself, whatever the tolerance, and a NaN to nothing. ``rel`` measures as
|
|
336
|
+
[`is_close_to()`][assertpy.numeric.NumericMixin.is_close_to] does, a fraction of the size of
|
|
337
|
+
``expected``, and beside ``tolerance`` either holding is enough.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
expected: the target value
|
|
341
|
+
tolerance: the allowed distance from ``expected``
|
|
342
|
+
rel: the maximum allowed difference as a fraction of the size of ``expected``, for numbers
|
|
343
|
+
"""
|
|
344
|
+
return CloseToMatcher(expected, tolerance, rel)
|
|
345
|
+
|
|
346
|
+
@staticmethod
|
|
347
|
+
def is_none() -> IsNoneMatcher:
|
|
348
|
+
"""Matcher for ``None``."""
|
|
349
|
+
return IsNoneMatcher()
|
|
350
|
+
|
|
351
|
+
@staticmethod
|
|
352
|
+
def is_not_none() -> IsNotNoneMatcher:
|
|
353
|
+
"""Matcher for any value that is not ``None``."""
|
|
354
|
+
return IsNotNoneMatcher()
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def is_instance_of(expected_type: ClassInfo) -> IsInstanceOfMatcher:
|
|
358
|
+
"""Matcher for an instance of ``expected_type`` (via ``isinstance``).
|
|
359
|
+
|
|
360
|
+
Accepts whatever ``isinstance`` accepts: a class, a union (``int | str``), or a tuple of
|
|
361
|
+
either. The builder assertion of the same name stays narrower on purpose - its overloads
|
|
362
|
+
refine the tracked value to the given class, and a union has no single class to refine to.
|
|
363
|
+
Reach for `is_instance_of_any` there.
|
|
364
|
+
"""
|
|
365
|
+
return IsInstanceOfMatcher(expected_type)
|
|
366
|
+
|
|
367
|
+
@staticmethod
|
|
368
|
+
def is_type_of(expected_type: type) -> IsTypeOfMatcher:
|
|
369
|
+
"""Matcher for exactly ``expected_type``, rejecting subclasses (``int`` but not ``bool``)."""
|
|
370
|
+
return IsTypeOfMatcher(expected_type)
|
|
371
|
+
|
|
372
|
+
@staticmethod
|
|
373
|
+
def is_truthy() -> IsTruthyMatcher:
|
|
374
|
+
"""Matcher for a truthy value."""
|
|
375
|
+
return IsTruthyMatcher()
|
|
376
|
+
|
|
377
|
+
@staticmethod
|
|
378
|
+
def is_falsy() -> IsFalsyMatcher:
|
|
379
|
+
"""Matcher for a falsy value."""
|
|
380
|
+
return IsFalsyMatcher()
|
|
381
|
+
|
|
382
|
+
@staticmethod
|
|
383
|
+
def has_length(length: SupportsIndex | Matcher[int]) -> HasLengthMatcher:
|
|
384
|
+
"""Matcher for a value whose ``len()`` equals ``length``, or satisfies it where it is a matcher."""
|
|
385
|
+
return HasLengthMatcher(length)
|
|
386
|
+
|
|
387
|
+
@staticmethod
|
|
388
|
+
def is_length(length: SupportsIndex | Matcher[int]) -> HasLengthMatcher:
|
|
389
|
+
"""Matcher for a value whose ``len()`` equals ``length``, or satisfies it where it is a matcher.
|
|
390
|
+
|
|
391
|
+
The same matcher as `has_length()`, under the name the fluent assertion uses
|
|
392
|
+
([`is_length()`][assertpy.base.BaseMixin.is_length]). One relation was reachable as
|
|
393
|
+
``has_length`` from the matcher namespace and as ``is_length`` from the builder, so which name
|
|
394
|
+
worked depended on which of the two a reader had seen first. Both work from both now.
|
|
395
|
+
"""
|
|
396
|
+
return HasLengthMatcher(length)
|
|
397
|
+
|
|
398
|
+
@staticmethod
|
|
399
|
+
def is_empty() -> IsEmptyMatcher:
|
|
400
|
+
"""Matcher for an empty value (``len() == 0``)."""
|
|
401
|
+
return IsEmptyMatcher()
|
|
402
|
+
|
|
403
|
+
@staticmethod
|
|
404
|
+
def is_not_empty() -> IsNotEmptyMatcher:
|
|
405
|
+
"""Matcher for a non-empty value (``len() > 0``)."""
|
|
406
|
+
return IsNotEmptyMatcher()
|
|
407
|
+
|
|
408
|
+
@staticmethod
|
|
409
|
+
def is_positive() -> IsPositiveMatcher:
|
|
410
|
+
"""Matcher for a value greater than zero."""
|
|
411
|
+
return IsPositiveMatcher()
|
|
412
|
+
|
|
413
|
+
@staticmethod
|
|
414
|
+
def is_negative() -> IsNegativeMatcher:
|
|
415
|
+
"""Matcher for a value less than zero."""
|
|
416
|
+
return IsNegativeMatcher()
|
|
417
|
+
|
|
418
|
+
@staticmethod
|
|
419
|
+
def is_zero() -> IsZeroMatcher:
|
|
420
|
+
"""Matcher for a value equal to zero."""
|
|
421
|
+
return IsZeroMatcher()
|
|
422
|
+
|
|
423
|
+
@staticmethod
|
|
424
|
+
def is_even() -> IsEvenMatcher:
|
|
425
|
+
"""Matcher for an even integer."""
|
|
426
|
+
return IsEvenMatcher()
|
|
427
|
+
|
|
428
|
+
@staticmethod
|
|
429
|
+
def is_odd() -> IsOddMatcher:
|
|
430
|
+
"""Matcher for an odd integer."""
|
|
431
|
+
return IsOddMatcher()
|
|
432
|
+
|
|
433
|
+
@staticmethod
|
|
434
|
+
def is_divisible_by(divisor: SupportsIndex) -> IsDivisibleByMatcher:
|
|
435
|
+
"""Matcher for an integer divisible by ``divisor``."""
|
|
436
|
+
return IsDivisibleByMatcher(divisor)
|
|
437
|
+
|
|
438
|
+
@staticmethod
|
|
439
|
+
def is_callable() -> IsCallableMatcher:
|
|
440
|
+
"""Matcher for a callable object."""
|
|
441
|
+
return IsCallableMatcher()
|
|
442
|
+
|
|
443
|
+
@staticmethod
|
|
444
|
+
def is_in(*values: object) -> IsInMatcher:
|
|
445
|
+
"""Matcher for a value present in ``values``.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
*values: the candidate values; the matched value must equal one of them
|
|
449
|
+
"""
|
|
450
|
+
return IsInMatcher(*values)
|
|
451
|
+
|
|
452
|
+
@staticmethod
|
|
453
|
+
def has_property(name: str, matcher: Matcher[Any] | None = None) -> HasPropertyMatcher:
|
|
454
|
+
"""Matcher for an object with attribute ``name``, optionally matching ``matcher``.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
name: the attribute name the object must have
|
|
458
|
+
matcher: optional matcher the attribute value must satisfy; if ``None``,
|
|
459
|
+
only the presence of the attribute is checked
|
|
460
|
+
"""
|
|
461
|
+
return HasPropertyMatcher(name, matcher)
|
|
462
|
+
|
|
463
|
+
@staticmethod
|
|
464
|
+
def contains_string(substring: str | bytes) -> _Composable[str | bytes]:
|
|
465
|
+
"""Matcher for text containing ``substring``, on `str` and on `bytes` alike."""
|
|
466
|
+
return ContainsStringMatcher(substring)
|
|
467
|
+
|
|
468
|
+
@staticmethod
|
|
469
|
+
def matches_regex(pattern: str | re.Pattern[str]) -> _Composable[str]:
|
|
470
|
+
"""Matcher for a string in which ``pattern`` (a string, or compiled from one) is found (``re.search``)."""
|
|
471
|
+
return MatchesRegexMatcher(pattern)
|
|
472
|
+
|
|
473
|
+
@staticmethod
|
|
474
|
+
def starts_with(prefix: str | bytes) -> _Composable[str | bytes]:
|
|
475
|
+
"""Matcher for text starting with ``prefix``, on `str` and on `bytes` alike."""
|
|
476
|
+
return StartsWithMatcher(prefix)
|
|
477
|
+
|
|
478
|
+
@staticmethod
|
|
479
|
+
def ends_with(suffix: str | bytes) -> _Composable[str | bytes]:
|
|
480
|
+
"""Matcher for text ending with ``suffix``, on `str` and on `bytes` alike."""
|
|
481
|
+
return EndsWithMatcher(suffix)
|
|
482
|
+
|
|
483
|
+
@staticmethod
|
|
484
|
+
def all_of(*matchers: Matcher[Any]) -> AllOfMatcher:
|
|
485
|
+
"""Matcher that holds when every one of ``matchers`` matches (the ``&`` operator)."""
|
|
486
|
+
return AllOfMatcher(*matchers)
|
|
487
|
+
|
|
488
|
+
@staticmethod
|
|
489
|
+
def any_of(*matchers: Matcher[Any]) -> AnyOfMatcher:
|
|
490
|
+
"""Matcher that holds when at least one of ``matchers`` matches (the ``|`` operator)."""
|
|
491
|
+
return AnyOfMatcher(*matchers)
|
|
492
|
+
|
|
493
|
+
@staticmethod
|
|
494
|
+
def not_(matcher: Matcher[Any]) -> NotMatcher:
|
|
495
|
+
"""Matcher that inverts ``matcher`` (the ``~`` operator)."""
|
|
496
|
+
return NotMatcher(matcher)
|
|
497
|
+
|
|
498
|
+
@staticmethod
|
|
499
|
+
def ignore() -> IgnoreMatcher:
|
|
500
|
+
"""Matcher that accepts anything; useful as a placeholder in ``structure`` specs."""
|
|
501
|
+
return IgnoreMatcher()
|
|
502
|
+
|
|
503
|
+
@staticmethod
|
|
504
|
+
def is_uuid() -> IsUuidMatcher:
|
|
505
|
+
"""Matcher for a string parseable as a UUID."""
|
|
506
|
+
return IsUuidMatcher()
|
|
507
|
+
|
|
508
|
+
@staticmethod
|
|
509
|
+
def is_non_empty_string() -> IsNonEmptyStringMatcher:
|
|
510
|
+
"""Matcher for a non-empty string."""
|
|
511
|
+
return IsNonEmptyStringMatcher()
|
|
512
|
+
|
|
513
|
+
@staticmethod
|
|
514
|
+
def is_now(delta: float | timedelta = 2.0) -> IsNowMatcher:
|
|
515
|
+
"""Matcher for a ``datetime`` within ``delta`` of the current time.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
delta: tolerance as seconds (a number) or a ``timedelta``, zero or more; defaults to 2 seconds.
|
|
519
|
+
Naive and timezone-aware values are both handled (compared against ``now`` in the same
|
|
520
|
+
awareness).
|
|
521
|
+
|
|
522
|
+
Raises:
|
|
523
|
+
ValueError: if delta is negative, or a number of seconds no ``timedelta`` holds
|
|
524
|
+
"""
|
|
525
|
+
if not isinstance(delta, timedelta):
|
|
526
|
+
# asked of the number: made a span, a tenth of a microsecond below nothing is none, and 1e20 overflows
|
|
527
|
+
if isinstance(delta, (int, float)) and delta < 0:
|
|
528
|
+
_assertpy_refusal = True
|
|
529
|
+
raise ValueError("given delta arg must not be negative")
|
|
530
|
+
try:
|
|
531
|
+
delta = timedelta(seconds=delta)
|
|
532
|
+
except (OverflowError, ValueError):
|
|
533
|
+
message = f"given delta arg must be a number of seconds a timedelta holds, but was <{delta}>"
|
|
534
|
+
_assertpy_refusal = True
|
|
535
|
+
raise ValueError(message) from None
|
|
536
|
+
return IsNowMatcher(delta)
|
|
537
|
+
|
|
538
|
+
@staticmethod
|
|
539
|
+
def is_before(other: datetime) -> IsBeforeMatcher:
|
|
540
|
+
"""Matcher for a ``datetime`` strictly before ``other`` (a non-comparable value never matches)."""
|
|
541
|
+
return IsBeforeMatcher(other)
|
|
542
|
+
|
|
543
|
+
@staticmethod
|
|
544
|
+
def is_after(other: datetime) -> IsAfterMatcher:
|
|
545
|
+
"""Matcher for a ``datetime`` strictly after ``other`` (a non-comparable value never matches)."""
|
|
546
|
+
return IsAfterMatcher(other)
|
|
547
|
+
|
|
548
|
+
@staticmethod
|
|
549
|
+
def contains(*items: _Item) -> _Composable[Iterable[_Item]]:
|
|
550
|
+
"""Matcher for a collection containing every one of ``items``.
|
|
551
|
+
|
|
552
|
+
The spec spelling of [`contains()`][assertpy.contains.ContainsMixin.contains], with the same
|
|
553
|
+
rules: a mapping is searched by key, and a matcher among the items is satisfied by any element.
|
|
554
|
+
|
|
555
|
+
Examples:
|
|
556
|
+
Usage:
|
|
557
|
+
|
|
558
|
+
assert_that(payload).matches_structure({"tags": match.contains("beta")})
|
|
559
|
+
assert_that(rows).satisfies(match.contains(match.greater_than(100)))
|
|
560
|
+
"""
|
|
561
|
+
return ContainsMatcher(*items)
|
|
562
|
+
|
|
563
|
+
@staticmethod
|
|
564
|
+
def contains_only(*items: _Item) -> _Composable[Iterable[_Item]]:
|
|
565
|
+
"""Matcher for a collection holding these items and nothing else.
|
|
566
|
+
|
|
567
|
+
The spec spelling of [`contains_only()`][assertpy.contains.ContainsMixin.contains_only].
|
|
568
|
+
"""
|
|
569
|
+
return ContainsOnlyMatcher(*items)
|
|
570
|
+
|
|
571
|
+
@overload
|
|
572
|
+
@staticmethod
|
|
573
|
+
def is_subset_of(superset: Iterable[_Item], /) -> _Composable[Iterable[_Item]]: ...
|
|
574
|
+
|
|
575
|
+
@overload
|
|
576
|
+
@staticmethod
|
|
577
|
+
def is_subset_of(*superset: _Item) -> _Composable[Iterable[_Item]]: ...
|
|
578
|
+
|
|
579
|
+
@staticmethod
|
|
580
|
+
def is_subset_of(*superset: object) -> _Composable[Iterable[Any]]:
|
|
581
|
+
"""Matcher for a collection whose items all appear in ``superset``.
|
|
582
|
+
|
|
583
|
+
The spec spelling of [`is_subset_of()`][assertpy.collection.CollectionMixin.is_subset_of].
|
|
584
|
+
Takes the superset either as one collection or as loose items, which is why it is overloaded:
|
|
585
|
+
read off a single union, a checker cannot tell `[1, 2]` the collection from `[1, 2]` the item.
|
|
586
|
+
|
|
587
|
+
An ordinary collection stays a live view of itself, the way ``equal_to`` keeps its expected
|
|
588
|
+
value; a one-shot iterator is drained when the matcher is built, since it could not answer a
|
|
589
|
+
second time otherwise. Handing in an endless iterator therefore never returns.
|
|
590
|
+
"""
|
|
591
|
+
return IsSubsetOfMatcher(*superset)
|
|
592
|
+
|
|
593
|
+
@staticmethod
|
|
594
|
+
def is_sorted(key: Callable[[Any], Any] | None = None, reverse: bool = False) -> _Composable[Iterable[Any]]:
|
|
595
|
+
"""Matcher for a collection in order, optionally by ``key`` and optionally reversed.
|
|
596
|
+
|
|
597
|
+
The spec spelling of [`is_sorted()`][assertpy.collection.CollectionMixin.is_sorted].
|
|
598
|
+
"""
|
|
599
|
+
return IsSortedMatcher(key, reverse)
|
|
600
|
+
|
|
601
|
+
@staticmethod
|
|
602
|
+
def each_item(matcher: Matcher[Any]) -> EachMatcher:
|
|
603
|
+
"""Matcher for an iterable whose every item matches ``matcher``.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
matcher: the matcher each item of the iterable must satisfy; a non-iterable
|
|
607
|
+
value never matches
|
|
608
|
+
"""
|
|
609
|
+
return EachMatcher(matcher)
|
|
610
|
+
|
|
611
|
+
@staticmethod
|
|
612
|
+
def structure(spec: dict[Any, Any]) -> StructureMatcher:
|
|
613
|
+
"""Matcher for a dict matching ``spec``.
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
spec: dict whose values are matchers, raw values (compared with ``==``),
|
|
617
|
+
or nested dict specs. Keys present in the value but absent from the spec are ignored.
|
|
618
|
+
|
|
619
|
+
Examples:
|
|
620
|
+
Usage:
|
|
621
|
+
|
|
622
|
+
assert_that(user).satisfies(
|
|
623
|
+
match.structure({"id": match.is_instance_of(int), "name": "Alice"})
|
|
624
|
+
)
|
|
625
|
+
"""
|
|
626
|
+
return StructureMatcher(spec)
|
|
627
|
+
|
|
628
|
+
def __getattr__(self, name: str) -> Callable[..., BaseMatcher]:
|
|
629
|
+
with _custom_matchers_lock:
|
|
630
|
+
try:
|
|
631
|
+
factory = _custom_matchers[name]
|
|
632
|
+
except KeyError:
|
|
633
|
+
message = f"match has no matcher {name!r}"
|
|
634
|
+
_assertpy_refusal = True
|
|
635
|
+
raise AttributeError(message) from None
|
|
636
|
+
return factory
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
match: _MatchNamespace = _MatchNamespace()
|