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.
Files changed (63) hide show
  1. assertpy/__init__.py +87 -0
  2. assertpy/_clustering.py +511 -0
  3. assertpy/_dangling.py +505 -0
  4. assertpy/_engine/__init__.py +1 -0
  5. assertpy/_engine/_builder_check_typing.py +1365 -0
  6. assertpy/_engine/_capable_typing.py +772 -0
  7. assertpy/_engine/_check_typing.py +753 -0
  8. assertpy/_engine/_compare.py +983 -0
  9. assertpy/_engine/_compat.py +38 -0
  10. assertpy/_engine/_contract.py +1926 -0
  11. assertpy/_engine/_diff.py +1058 -0
  12. assertpy/_engine/_equality.py +843 -0
  13. assertpy/_engine/_introspection.py +729 -0
  14. assertpy/_engine/_membership.py +522 -0
  15. assertpy/_engine/_mixin_base.py +258 -0
  16. assertpy/_engine/_negated_typing.py +2176 -0
  17. assertpy/_engine/_operations.py +75 -0
  18. assertpy/_engine/_ordering.py +884 -0
  19. assertpy/_engine/_pairing.py +70 -0
  20. assertpy/_engine/_path.py +158 -0
  21. assertpy/_engine/_poll_typing.py +6039 -0
  22. assertpy/_engine/_require.py +312 -0
  23. assertpy/_engine/_size.py +31 -0
  24. assertpy/_engine/_text.py +39 -0
  25. assertpy/_engine/_typing.py +1226 -0
  26. assertpy/_hints.py +1213 -0
  27. assertpy/_inline.py +484 -0
  28. assertpy/_matcher_impls.py +1876 -0
  29. assertpy/_passes.py +249 -0
  30. assertpy/_renamed.py +64 -0
  31. assertpy/_satisfies.py +835 -0
  32. assertpy/_snapshot_codec.py +290 -0
  33. assertpy/assertpy.py +2640 -0
  34. assertpy/async_assertions.py +864 -0
  35. assertpy/base.py +862 -0
  36. assertpy/behave_matchers.py +96 -0
  37. assertpy/bytes_mixin.py +158 -0
  38. assertpy/collection.py +528 -0
  39. assertpy/contains.py +1068 -0
  40. assertpy/dataframe.py +200 -0
  41. assertpy/date.py +327 -0
  42. assertpy/dict.py +249 -0
  43. assertpy/dynamic.py +222 -0
  44. assertpy/errors.py +782 -0
  45. assertpy/exception.py +838 -0
  46. assertpy/extracting.py +266 -0
  47. assertpy/file.py +324 -0
  48. assertpy/helpers.py +747 -0
  49. assertpy/http_mixin.py +218 -0
  50. assertpy/json_mixin.py +1265 -0
  51. assertpy/matchers.py +639 -0
  52. assertpy/numeric.py +696 -0
  53. assertpy/outcome.py +163 -0
  54. assertpy/py.typed +0 -0
  55. assertpy/pytest_plugin.py +1650 -0
  56. assertpy/snapshot.py +1031 -0
  57. assertpy/string.py +803 -0
  58. assertpy/warning.py +137 -0
  59. assertpy-2.35.0.dist-info/METADATA +324 -0
  60. assertpy-2.35.0.dist-info/RECORD +63 -0
  61. assertpy-2.35.0.dist-info/WHEEL +4 -0
  62. assertpy-2.35.0.dist-info/entry_points.txt +2 -0
  63. 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()