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/_satisfies.py ADDED
@@ -0,0 +1,835 @@
1
+ from __future__ import annotations
2
+
3
+ import collections.abc
4
+ import warnings
5
+ from typing import TYPE_CHECKING, Any, cast
6
+
7
+ from . import _renamed
8
+ from ._engine._diff import _walk_leaves
9
+ from ._engine._introspection import (
10
+ caller_stacklevel,
11
+ class_name,
12
+ is_attrs_instance,
13
+ is_mapping_like,
14
+ is_model_dump_object,
15
+ plainly_hashed,
16
+ )
17
+ from ._engine._mixin_base import _MixinBase
18
+ from ._engine._pairing import maximum_pairing
19
+ from ._engine._path import _ROOT
20
+ from ._engine._require import VerdictError, argument, refuse, verdict
21
+ from ._hints import class_in_spec, under
22
+ from ._matcher_impls import _A_STRUCTURE, _describe_spec_value, _has_own_evaluate, _outcome_of, _ReadOnce
23
+ from .errors import (
24
+ DiffEntry,
25
+ DiffResult,
26
+ VacuousAssertionWarning,
27
+ _capped,
28
+ _capped_format,
29
+ _safe_repr,
30
+ _truncated,
31
+ )
32
+ from .helpers import _ELIDED, _entry_part, _field_part, _joined_parts, _Part
33
+ from .matchers import (
34
+ BaseMatcher,
35
+ IsNotNoneMatcher,
36
+ Matcher,
37
+ StructureMatcher,
38
+ _apply_matcher,
39
+ _describe_matcher,
40
+ _evaluate_matcher,
41
+ _is_matcher,
42
+ _refused,
43
+ _resolved,
44
+ )
45
+
46
+ if TYPE_CHECKING:
47
+ from collections.abc import Callable, Iterable
48
+
49
+ from ._engine._compat import Self
50
+
51
+ __tracebackhide__ = True
52
+
53
+
54
+ _Kept = dict[int, tuple[object, Any]]
55
+ """The keys of one level of a spec that lead to a mismatch, by the identity of the key, each with the level under it."""
56
+
57
+
58
+ def _kept_keys(entries: list[DiffEntry]) -> _Kept:
59
+ """The keys on the way to every mismatch of a spec, level by level, read off the steps its walk recorded."""
60
+ kept: _Kept = {}
61
+ for entry in entries:
62
+ level = kept
63
+ for step in entry.steps:
64
+ level = level.setdefault(id(step.value), (step.value, {}))[1]
65
+ return kept
66
+
67
+
68
+ def _elided_spec(expected: object, kept: _Kept) -> str:
69
+ """What a spec value is described as, with the keys that matched left out as ``..``.
70
+
71
+ Whole where nothing under it failed, and where the spec there is no exact dict: its keys are found by
72
+ identity, and a dict of a class of its own may hand out other objects each time it is read.
73
+ """
74
+ spec = expected._spec if type(expected) is StructureMatcher else expected
75
+ if not kept or type(spec) is not dict:
76
+ return _describe_spec_value(expected)
77
+ parts: list[_Part] = []
78
+ pending = False
79
+ for key, value in spec.items():
80
+ found = kept.get(id(key))
81
+ if found is None:
82
+ pending = True
83
+ continue
84
+ if pending:
85
+ parts.append(_ELIDED)
86
+ pending = False
87
+ parts.append(f"{key}: {_elided_spec(value, found[1])}")
88
+ if pending:
89
+ parts.append(_ELIDED)
90
+ described = _joined_parts(parts, opener="{", closer="}")
91
+ return described if spec is expected else f"{_A_STRUCTURE} {described}"
92
+
93
+
94
+ def _elided_mapping(
95
+ mapping: object, kept: _Kept, opener: str, closer: str, part_of: Callable[[object, str], str]
96
+ ) -> str | None:
97
+ """*mapping* as a failed spec leaves it in a message: the keys on the way to a mismatch, ``..`` for the rest.
98
+
99
+ ``None`` where it is not read that way: nothing under it failed, it is no exact dict, or a key of it or of
100
+ the spec's on the way is of a class whose lookup runs that class's ``__hash__`` or ``__eq__``. And where it
101
+ is written as a record and a key to show is no name: ``x=1, y`` as a field would read as two.
102
+ """
103
+ if not kept or type(mapping) is not dict:
104
+ return None
105
+ if not all(plainly_hashed(key) for key, _ in kept.values()) or not all(map(plainly_hashed, mapping)):
106
+ return None
107
+ below_of = dict(kept.values())
108
+ parts: list[_Part] = []
109
+ pending = False
110
+ # read before anything is printed: the repr of a value may take a key out of the dict that holds it
111
+ for key, value in list(mapping.items()):
112
+ below = below_of.get(key)
113
+ if below is None:
114
+ pending = True
115
+ continue
116
+ if part_of is _field_part and not (type(key) is str and key.isidentifier()):
117
+ return None
118
+ if pending:
119
+ parts.append(_ELIDED)
120
+ pending = False
121
+ nested = _elided_mapping(value, below, "{", "}", _entry_part)
122
+ parts.append(part_of(key, _safe_repr(value) if nested is None else nested))
123
+ if pending:
124
+ parts.append(_ELIDED)
125
+ return _joined_parts(parts, opener=opener, closer=closer)
126
+
127
+
128
+ def _describe_unpaired(matcher, raised_count):
129
+ """Describe an unpaired matcher, noting raised probes so a buggy predicate is not mistaken for a mismatch."""
130
+ description = _describe_matcher(matcher)
131
+ if raised_count:
132
+ item_word = "item" if raised_count == 1 else "items"
133
+ return f"{description} (probe raised TypeError on {raised_count} {item_word})"
134
+ return description
135
+
136
+
137
+ def _max_bipartite_assignment(satisfied: list[list[bool]]) -> list[int | None]:
138
+ """Pair each row with a distinct satisfying column (None where no pairing reaches it)."""
139
+ column_owner = maximum_pairing([{column for column, ok in enumerate(row) if ok} for row in satisfied])
140
+ assignment: list[int | None] = [None] * len(satisfied)
141
+ for column, row in column_owner.items():
142
+ assignment[row] = column
143
+ return assignment
144
+
145
+
146
+ _TRUTHY = frozenset({"1", "true", "yes", "on"})
147
+
148
+
149
+ def _env_enabled() -> bool:
150
+ """Read the environment switch, at import and from the pytest plugin rather than per assertion.
151
+
152
+ The switch cannot change within a process, and the lookup with its two string operations costs
153
+ around 300ns: more than the emptiness check it decides to skip, on every quantifier call.
154
+ """
155
+ return _renamed.environ("ASSERTPY_VACUOUS").strip().lower() in _TRUTHY
156
+
157
+
158
+ _VACUOUS_GUARD: bool = _env_enabled()
159
+ """Whether a universal assertion over an empty value warns.
160
+
161
+ Off by default: a suite running ``filterwarnings = ["error"]`` would otherwise fail on upgrade, and a
162
+ property-based test generates empty collections as a matter of course. Turn it on with the pytest
163
+ ``--assertpy-vacuous`` flag or the ``ASSERTPY_VACUOUS`` environment variable for other runners.
164
+ """
165
+
166
+
167
+ def _guarding(builder: _MixinBase) -> bool:
168
+ """Whether an assertion on *builder* that checked nothing should say so now.
169
+
170
+ Not when another assertion runs it for its own answer. Under `not_` its pass is the failure the
171
+ caller gets, and the warning beside it pointed into the library. Under a snapshot the keys it
172
+ ignores include the placeholders the snapshot checks with matchers of their own. Kept on the
173
+ builder, so an assertion a callback of the caller's runs on a builder of its own still speaks.
174
+ """
175
+ return _VACUOUS_GUARD and not builder._answering_another
176
+
177
+
178
+ def _warn_nothing_compared() -> None:
179
+ """Say that `is_equal_to()` passed with ``ignore`` and ``include`` leaving no key to compare."""
180
+ warnings.warn(
181
+ "is_equal_to() passed with ignore and include leaving no key to compare, so nothing was checked.",
182
+ VacuousAssertionWarning,
183
+ stacklevel=caller_stacklevel(),
184
+ )
185
+
186
+
187
+ def _warn_vacuous(builder: _MixinBase, name: str, allow_empty: bool) -> None:
188
+ """Say that *name* passed over nothing, pointing at the line that called the assertion.
189
+
190
+ Called by the assertion itself once its walk is over, never before it. Asking the subject first
191
+ was the earlier design and it went wrong in every direction available: it spoke in front of a
192
+ refusal, it took an item from a value whose walks share one position, and it answered about a
193
+ length where the assertion was about to walk fields. What was walked is the only thing that
194
+ answers "nothing was checked", and only the walk knows it.
195
+ """
196
+ if allow_empty or not _guarding(builder):
197
+ return
198
+ warnings.warn(
199
+ f"{name}() passed over an empty value, so nothing was checked. Pass allow_empty=True if that is intended.",
200
+ VacuousAssertionWarning,
201
+ stacklevel=caller_stacklevel(),
202
+ )
203
+
204
+
205
+ class SatisfiesMixin(_MixinBase):
206
+ """Predicate and matcher-application assertions: satisfies / each / *_satisfy / matches_structure."""
207
+
208
+ def all_fields_satisfy(self, matcher: Matcher[Any] | Callable[..., bool], *, allow_empty: bool = False) -> Self:
209
+ """Asserts that every scalar leaf in val's object graph satisfies the given matcher.
210
+
211
+ Walks val recursively (mappings, dataclasses, namedtuples, Pydantic models, lists, tuples) and
212
+ applies the matcher to each leaf value, reporting the path of every leaf that does not satisfy it.
213
+ Scalars, strings, sets and opaque objects are treated as single leaves. A Pydantic model is
214
+ walked through the values its fields hold, not its ``model_dump()``: a field excluded from the
215
+ dump is walked, a computed field is not, and a field serializer does not apply.
216
+
217
+ Args:
218
+ matcher: a `Matcher` or callable predicate applied to every leaf
219
+
220
+ Examples:
221
+ Usage:
222
+
223
+ from assertpy import match
224
+
225
+ assert_that({"a": 1, "nested": {"b": 2}}).all_fields_satisfy(match.is_positive())
226
+ assert_that([1, [2, 3]]).all_fields_satisfy(lambda x: x > 0)
227
+
228
+ Returns:
229
+ AssertionBuilder: returns this instance to chain to the next assertion
230
+
231
+ Raises:
232
+ AssertionError: if any leaf does **not** satisfy the matcher
233
+ TypeError: if matcher is neither a Matcher nor callable
234
+ """
235
+ return self._every_field(matcher, allow_empty, "all_fields_satisfy")
236
+
237
+ def _every_field(self, matcher, allow_empty: bool, name: str) -> Self:
238
+ """The walk both field quantifiers make, named by whichever of them asked for it."""
239
+ description = None
240
+ walked = 0
241
+ failures = []
242
+ reading = _resolved(matcher)
243
+ apply = reading.apply # read once: the attribute lookup is per leaf otherwise
244
+ for path, leaf in _walk_leaves(self.val):
245
+ walked += 1
246
+ if not apply(leaf):
247
+ # asked on the first refusal: a self-describing matcher would otherwise read the subject first
248
+ description = reading.describe() if description is None else description
249
+ failures.append(path.leaf_entry(actual=leaf, expected=description))
250
+ if failures:
251
+ count = len(failures)
252
+ field_word = "field" if count == 1 else "fields"
253
+ return self.error(
254
+ f"Expected all fields to satisfy {description}, but {count} {field_word} did not.",
255
+ actual=self.val,
256
+ expected=description,
257
+ diff=DiffResult(kind="match", entries=failures),
258
+ )
259
+ if not walked:
260
+ _warn_vacuous(self, name, allow_empty)
261
+ return self
262
+
263
+ def has_no_none_fields(self, *, allow_empty: bool = False) -> Self:
264
+ """Asserts that no scalar leaf in val's object graph is ``None``.
265
+
266
+ Convenience wrapper over `all_fields_satisfy()` with a not-``None`` matcher; reports the path
267
+ of every ``None`` leaf found anywhere in the graph, reading a Pydantic model as that does.
268
+
269
+ Examples:
270
+ Usage:
271
+
272
+ assert_that({"id": 1, "profile": {"name": "Alice"}}).has_no_none_fields()
273
+
274
+ Returns:
275
+ AssertionBuilder: returns this instance to chain to the next assertion
276
+
277
+ Raises:
278
+ AssertionError: if any leaf **is** ``None``
279
+ """
280
+ return self._every_field(IsNotNoneMatcher(), allow_empty, "has_no_none_fields")
281
+
282
+ def satisfies(self, matcher: Matcher[Any] | Callable[..., bool]) -> Self:
283
+ """Asserts that val satisfies the given matcher.
284
+
285
+ Args:
286
+ matcher: a `Matcher` instance, or a callable that takes
287
+ a value and returns a bool
288
+
289
+ Examples:
290
+ Usage with matchers:
291
+
292
+ from assertpy import match
293
+
294
+ assert_that(7).satisfies(match.greater_than(5) & match.less_than(10))
295
+ assert_that('hello').satisfies(match.starts_with('he'))
296
+
297
+ Usage with callables:
298
+
299
+ assert_that(42).satisfies(lambda x: x % 2 == 0)
300
+
301
+ When the callable is typed with ``TypeIs`` it also *narrows* the chain to the guarded type
302
+ (refinement narrowing), so ``.value`` hands the value back typed - see
303
+ [Type Safety](../concepts/type-safety.md#refinement-narrowing-with-a-typeis-predicate-advanced) for
304
+ checker support (advanced; not yet in PyCharm).
305
+
306
+ Returns:
307
+ AssertionBuilder: returns this instance to chain to the next assertion
308
+
309
+ Raises:
310
+ AssertionError: if val does **not** satisfy the matcher
311
+ """
312
+ if _is_matcher(matcher):
313
+ read = _ReadOnce(self.val, self._drained, self._handed_out)
314
+ result = _outcome_of(matcher, read)
315
+ if result is not None:
316
+ value = read.shown()
317
+ return self.error(
318
+ f"Expected {result.description}, but {result.mismatch}.",
319
+ actual=value,
320
+ expected=result.description,
321
+ diff=DiffResult(
322
+ kind="match", entries=[_ROOT.leaf_entry(actual=value, expected=result.description)]
323
+ ),
324
+ )
325
+ elif callable(matcher):
326
+ if not verdict(cast("Callable[..., object]", matcher)(self._handed_out())):
327
+ return self.error(
328
+ f"Expected <{_capped(self.val)}> to satisfy {_describe_matcher(matcher)}, but did not.",
329
+ expected=_describe_matcher(matcher),
330
+ )
331
+ else:
332
+ refuse(matcher, "a Matcher or a callable", subject=argument("matcher"))
333
+ return self
334
+
335
+ def each(self, matcher: Matcher[Any] | Callable[..., bool], *, allow_empty: bool = False) -> Self:
336
+ """Asserts that every item in val satisfies the given matcher.
337
+
338
+ Args:
339
+ matcher: a `Matcher` instance, or a callable that takes
340
+ a value and returns a bool
341
+
342
+ Examples:
343
+ Usage with matchers:
344
+
345
+ from assertpy import match
346
+
347
+ assert_that([1, 2, 3]).each(match.is_positive())
348
+ assert_that([10, 20, 30]).each(match.between(1, 100))
349
+
350
+ Usage with extracting:
351
+
352
+ assert_that(users).extracting('age').each(match.between(18, 120))
353
+
354
+ Returns:
355
+ AssertionBuilder: returns this instance to chain to the next assertion
356
+
357
+ Raises:
358
+ AssertionError: if any item does **not** satisfy the matcher
359
+ """
360
+ if not _is_matcher(matcher) and not callable(matcher):
361
+ refuse(matcher, "a Matcher or a callable", subject=argument("matcher"))
362
+ if not isinstance(self.val, collections.abc.Iterable):
363
+ refuse(self.val, "iterable")
364
+ return self._every_item(matcher, allow_empty, "each")
365
+
366
+ def _every_item(self, matcher, allow_empty: bool, name: str) -> Self:
367
+ """The walk both item quantifiers make, named by whichever of them asked for it."""
368
+ walked = 0
369
+ if _is_matcher(matcher):
370
+ # a structure is asked for its failure alone, so the spec is described only for the item that failed
371
+ structural = getattr(type(matcher), "evaluate", None) is StructureMatcher.evaluate
372
+ for i, item in enumerate(self._walked()):
373
+ walked += 1
374
+ # the verdict and the reason come from the same look, so a user's `key` runs once
375
+ if structural or _has_own_evaluate(matcher):
376
+ outcome = (
377
+ cast("StructureMatcher", matcher)._failure(item)
378
+ if structural
379
+ else _evaluate_matcher(matcher, item)
380
+ )
381
+ elif verdict(matcher.matches(item), subject="the matcher"):
382
+ continue
383
+ else:
384
+ outcome = _refused(matcher, item)
385
+ if outcome is not None and not outcome.matched:
386
+ # read off the refusal: the matcher said what it wanted, and asking twice ran its own method twice
387
+ description = outcome.description
388
+ return self.error(
389
+ f"Expected all items to satisfy {description},"
390
+ f" but item at index {i} <{_capped_format(item)}> did not:"
391
+ f" {outcome.mismatch}.",
392
+ actual=item,
393
+ expected=description,
394
+ diff=DiffResult(
395
+ kind="match", entries=[_ROOT.index(i).entry(actual=item, expected=description)]
396
+ ),
397
+ )
398
+ else:
399
+ for i, item in enumerate(self._walked()):
400
+ walked += 1
401
+ if not verdict(cast("Callable[..., object]", matcher)(item)):
402
+ return self.error(
403
+ f"Expected all items to satisfy {_describe_matcher(matcher)},"
404
+ f" but item at index {i} <{_capped_format(item)}> did not.",
405
+ expected=_describe_matcher(matcher),
406
+ )
407
+ if not walked:
408
+ _warn_vacuous(self, name, allow_empty)
409
+ return self
410
+
411
+ def matches_structure(self, spec: dict[Any, Any]) -> Self:
412
+ """Asserts that val matches the given structure specification.
413
+
414
+ ``val`` may be any mapping (a ``dict``, a ``MappingProxyType``, 3.15's ``frozendict``, or your
415
+ own ``collections.abc.Mapping``), a pydantic-style model (anything exposing ``model_dump()``),
416
+ or an ``attrs`` instance, which is normalized to its dict before matching. Each key in
417
+ ``spec`` maps to either a `Matcher`, a raw value (checked via ``==``), or a nested ``dict``
418
+ for recursive matching. Extra keys in val that are absent from the spec are allowed.
419
+
420
+ A failure prints the spec with what matched left out as ``..``, and val the same way where it is read
421
+ as plain dicts with plain keys, a model and an attrs instance among them. It holds both whole.
422
+
423
+ Args:
424
+ spec: a dict where values can be Matcher instances, raw values, or nested dicts
425
+
426
+ Examples:
427
+ Usage:
428
+
429
+ from assertpy import assert_that, match
430
+
431
+ user = {"name": "Alice", "age": 30, "id": "550e8400-e29b-41d4-a716-446655440000"}
432
+ assert_that(user).matches_structure({
433
+ "name": match.is_non_empty_string(),
434
+ "age": match.between(18, 120),
435
+ "id": match.is_uuid(),
436
+ })
437
+
438
+ Returns:
439
+ AssertionBuilder: returns this instance to chain to the next assertion
440
+
441
+ Raises:
442
+ AssertionError: if val does **not** match the structure spec
443
+ """
444
+ if not is_mapping_like(self.val) and not is_model_dump_object(self.val) and not is_attrs_instance(self.val):
445
+ refuse(self.val, "a mapping, a pydantic-style model, or an attrs instance")
446
+ if not isinstance(spec, dict):
447
+ refuse(spec, "a dict", subject=argument("spec"))
448
+ matcher = StructureMatcher(spec)
449
+ mapped = matcher._as_mapping(self.val)
450
+ # one walk, read twice: the entries want every mismatch, the message wants the first one in words
451
+ mismatches = matcher._walk(mapped, spec, _ROOT, set())
452
+ if mismatches:
453
+ entries = [
454
+ mismatch.path.entry(actual=mismatch.actual, expected=mismatch.expected_desc) for mismatch in mismatches
455
+ ]
456
+ kept = _kept_keys(entries)
457
+ spelling = (
458
+ ("{", "}", _entry_part) if mapped is self.val else (f"{class_name(type(self.val))}(", ")", _field_part)
459
+ )
460
+ shown = _elided_mapping(mapped, kept, *spelling)
461
+ return self.error(
462
+ f"Expected <{_capped(self.val) if shown is None else _truncated(shown)}> to match structure"
463
+ f" {_elided_spec(spec, kept)}, but"
464
+ f" {matcher.render_mismatch(mismatches)}."
465
+ f"{under(class_in_spec((one.path.text, one.actual, one.literal) for one in mismatches))}",
466
+ actual=self.val,
467
+ expected=spec,
468
+ diff=DiffResult(kind="match", entries=entries),
469
+ )
470
+ return self
471
+
472
+ def is_callable(self) -> Self:
473
+ """Asserts that val is callable.
474
+
475
+ Examples:
476
+ Usage:
477
+
478
+ assert_that(lambda: None).is_callable()
479
+ assert_that(print).is_callable()
480
+
481
+ Returns:
482
+ AssertionBuilder: returns this instance to chain to the next assertion
483
+
484
+ Raises:
485
+ AssertionError: if val is **not** callable
486
+ """
487
+ if not callable(self.val):
488
+ return self.error(f"Expected <{_capped(self.val)}> to be callable, but was not.")
489
+ return self
490
+
491
+ def is_not_callable(self) -> Self:
492
+ """Asserts that val is not callable.
493
+
494
+ Examples:
495
+ Usage:
496
+
497
+ assert_that(42).is_not_callable()
498
+ assert_that('foo').is_not_callable()
499
+
500
+ Returns:
501
+ AssertionBuilder: returns this instance to chain to the next assertion
502
+
503
+ Raises:
504
+ AssertionError: if val **is** callable
505
+ """
506
+ if callable(self.val):
507
+ return self.error(f"Expected <{_capped(self.val)}> to not be callable, but was.")
508
+ return self
509
+
510
+ def any_satisfy(self, matcher: Matcher[Any] | Callable[..., bool]) -> Self:
511
+ """Asserts that at least one item in val satisfies the given matcher.
512
+
513
+ Args:
514
+ matcher: a `Matcher` instance, or a callable that takes
515
+ a value and returns a bool
516
+
517
+ Examples:
518
+ Usage with matchers:
519
+
520
+ from assertpy import match
521
+
522
+ assert_that([1, -2, 3]).any_satisfy(match.is_negative())
523
+
524
+ Usage with callables:
525
+
526
+ assert_that([1, 2, 3]).any_satisfy(lambda x: x > 2)
527
+
528
+ Returns:
529
+ AssertionBuilder: returns this instance to chain to the next assertion
530
+
531
+ Raises:
532
+ AssertionError: if no item satisfies the matcher
533
+ """
534
+ if not isinstance(self.val, collections.abc.Iterable):
535
+ refuse(self.val, "iterable")
536
+ # resolving is what refuses, so the matcher is inspected once rather than here and again there
537
+ reading = _resolved(matcher)
538
+ apply = reading.apply # read once: the attribute lookup is per item otherwise
539
+ values = self._drained()
540
+ # kept as the verdict walks them: the message names them, and a second walk is a read the verdict never made
541
+ items = []
542
+ for item in values:
543
+ if apply(item):
544
+ return self
545
+ items.append(item)
546
+ # asked once the verdict is in, since it is wanted only for the message
547
+ description = reading.describe()
548
+ return self.error(
549
+ f"Expected any item to satisfy {description}, but none of the {len(items)} did.",
550
+ actual=values,
551
+ expected=description,
552
+ diff=DiffResult(
553
+ kind="match",
554
+ entries=[
555
+ _ROOT.index(index).entry(actual=item, expected=description) for index, item in enumerate(items[:5])
556
+ ],
557
+ ),
558
+ )
559
+
560
+ def all_satisfy(self, matcher: Matcher[Any] | Callable[..., bool], *, allow_empty: bool = False) -> Self:
561
+ """Asserts that all items in val satisfy the given matcher.
562
+
563
+ Semantic alias for [`each()`][assertpy.base.BaseMixin.each].
564
+
565
+ Args:
566
+ matcher: a `Matcher` instance, or a callable that takes
567
+ a value and returns a bool
568
+
569
+ Examples:
570
+ Usage with matchers:
571
+
572
+ from assertpy import match
573
+
574
+ assert_that([1, 2, 3]).all_satisfy(match.is_positive())
575
+
576
+ Returns:
577
+ AssertionBuilder: returns this instance to chain to the next assertion
578
+
579
+ Raises:
580
+ AssertionError: if any item does **not** satisfy the matcher
581
+ """
582
+ if not _is_matcher(matcher) and not callable(matcher):
583
+ refuse(matcher, "a Matcher or a callable", subject=argument("matcher"))
584
+ if not isinstance(self.val, collections.abc.Iterable):
585
+ refuse(self.val, "iterable")
586
+ return self._every_item(matcher, allow_empty, "all_satisfy")
587
+
588
+ def none_satisfy(self, matcher: Matcher[Any] | Callable[..., bool]) -> Self:
589
+ """Asserts that no item in val satisfies the given matcher.
590
+
591
+ Args:
592
+ matcher: a `Matcher` instance, or a callable that takes
593
+ a value and returns a bool
594
+
595
+ Examples:
596
+ Usage with matchers:
597
+
598
+ from assertpy import match
599
+
600
+ assert_that([1, 2, 3]).none_satisfy(match.is_negative())
601
+
602
+ Usage with callables:
603
+
604
+ assert_that([1, 2, 3]).none_satisfy(lambda x: x < 0)
605
+
606
+ Returns:
607
+ AssertionBuilder: returns this instance to chain to the next assertion
608
+
609
+ Raises:
610
+ AssertionError: if any item satisfies the matcher
611
+ """
612
+ if not isinstance(self.val, collections.abc.Iterable):
613
+ refuse(self.val, "iterable")
614
+ if _is_matcher(matcher):
615
+ for i, item in enumerate(self._walked()):
616
+ if verdict(matcher.matches(item), subject="the matcher"):
617
+ return self.error(
618
+ f"Expected no item to satisfy {matcher.describe()},"
619
+ f" but item at index {i} <{_capped_format(item)}> did."
620
+ )
621
+ elif callable(matcher):
622
+ for i, item in enumerate(self._walked()):
623
+ if verdict(cast("Callable[..., object]", matcher)(item)):
624
+ return self.error(
625
+ f"Expected no item to satisfy {_describe_matcher(matcher)},"
626
+ f" but item at index {i} <{_capped_format(item)}> did."
627
+ )
628
+ else:
629
+ refuse(matcher, "a Matcher or a callable", subject=argument("matcher"))
630
+ return self
631
+
632
+ def satisfies_exactly(self, *matchers: Matcher[Any] | Callable[..., bool]) -> Self:
633
+ """Asserts that val has exactly one item per matcher, each satisfying the matcher at its position.
634
+
635
+ Unlike [`each()`][assertpy.base.BaseMixin.each] (one matcher applied to every item), this
636
+ pairs the i-th item with the
637
+ i-th matcher and additionally requires the lengths to match. Every positional mismatch is
638
+ reported, not just the first.
639
+
640
+ Args:
641
+ *matchers: one `Matcher` or callable predicate per expected item,
642
+ applied in order
643
+
644
+ Examples:
645
+ Usage:
646
+
647
+ from assertpy import match
648
+
649
+ assert_that([1, "foo", 3.0]).satisfies_exactly(
650
+ match.is_odd(), match.is_instance_of(str), match.is_positive()
651
+ )
652
+ assert_that([2, 4]).satisfies_exactly(lambda x: x == 2, lambda x: x == 4)
653
+
654
+ Returns:
655
+ AssertionBuilder: returns this instance to chain to the next assertion
656
+
657
+ Raises:
658
+ AssertionError: if the length differs, or any item does **not** satisfy its matcher
659
+ TypeError: if val is not iterable, or a given arg is neither a Matcher nor callable
660
+ ValueError: if no matchers are given
661
+ """
662
+ if len(matchers) == 0:
663
+ raise ValueError("one or more args must be given")
664
+ # before the value is read, as the any-order twin: the length decided, and `not_` passed an argument like `123`
665
+ for matcher in matchers:
666
+ if not isinstance(matcher, BaseMatcher) and not callable(matcher) and not _is_matcher(matcher):
667
+ refuse(matcher, "a Matcher or a callable", subject=argument("matcher"))
668
+ if not isinstance(self.val, collections.abc.Iterable):
669
+ refuse(self.val, "iterable")
670
+ items = list(self._walked())
671
+ if len(items) != len(matchers):
672
+ return self.error(
673
+ f"Expected collection length <{len(matchers)}>, but was <{len(items)}>.",
674
+ actual=self.val,
675
+ expected=[_describe_matcher(matcher) for matcher in matchers],
676
+ )
677
+ entries = [
678
+ _ROOT.index(index).entry(actual=item, expected=_describe_matcher(matcher))
679
+ for index, (item, matcher) in enumerate(zip(items, matchers, strict=True))
680
+ if not _apply_matcher(matcher, item)
681
+ ]
682
+ if entries:
683
+ failed = "item" if len(entries) == 1 else "items"
684
+ return self.error(
685
+ f"Expected items to satisfy the given matchers in order, but {len(entries)} {failed} did not.",
686
+ actual=self.val,
687
+ expected=[_describe_matcher(matcher) for matcher in matchers],
688
+ diff=DiffResult(kind="match", entries=entries),
689
+ )
690
+ return self
691
+
692
+ def satisfies_exactly_in_any_order(self, *matchers: Matcher[Any] | Callable[..., bool]) -> Self:
693
+ """Asserts that the items and the given matchers can be paired one-to-one, in any order.
694
+
695
+ Like [`satisfies_exactly()`][assertpy.base.BaseMixin.satisfies_exactly] but ignoring
696
+ positions: passes when some one-to-one assignment pairs every item with a distinct matcher
697
+ it satisfies. The lengths must still match, and no matcher may be reused for two items.
698
+
699
+ Since every matcher is probed against every item, a probe that raises ``TypeError`` (a
700
+ type-incompatible comparison on a mixed collection, like ``is_positive()`` meeting a string)
701
+ counts as a non-match instead of crashing the pairing. On failure, an unpaired matcher whose
702
+ probes raised is annotated with the raise count in the diff, so a buggy predicate is not
703
+ mistaken for an ordinary mismatch.
704
+
705
+ Args:
706
+ *matchers: one `Matcher` or callable predicate per expected item, in any order
707
+
708
+ Examples:
709
+ Usage:
710
+
711
+ from assertpy import match
712
+
713
+ assert_that(["foo", 3]).satisfies_exactly_in_any_order(
714
+ match.is_odd(), match.is_instance_of(str)
715
+ )
716
+ assert_that([2, 1]).satisfies_exactly_in_any_order(lambda x: x == 1, lambda x: x == 2)
717
+
718
+ Returns:
719
+ AssertionBuilder: returns this instance to chain to the next assertion
720
+
721
+ Raises:
722
+ AssertionError: if the length differs, or no one-to-one pairing satisfies all matchers
723
+ TypeError: if val is not iterable, or a given arg is neither a Matcher nor callable
724
+ ValueError: if no matchers are given
725
+ """
726
+ if len(matchers) == 0:
727
+ raise ValueError("one or more args must be given")
728
+ # resolved here rather than asked again per item, and the resolution is what refuses a bad one
729
+ appliers = [reading.apply for reading in (_resolved(matcher) for matcher in matchers)]
730
+ if not isinstance(self.val, collections.abc.Iterable):
731
+ refuse(self.val, "iterable")
732
+ items = list(self._walked())
733
+ if len(items) != len(matchers):
734
+ return self.error(
735
+ f"Expected collection length <{len(matchers)}>, but was <{len(items)}>.",
736
+ actual=self.val,
737
+ expected=[_describe_matcher(matcher) for matcher in matchers],
738
+ )
739
+ raised_counts = [0] * len(matchers)
740
+ satisfied = []
741
+ for item in items:
742
+ row = []
743
+ for column, apply in enumerate(appliers):
744
+ try:
745
+ row.append(apply(item))
746
+ except VerdictError: # noqa: PERF203 # never a non-match, always a mistake
747
+ raise
748
+ except TypeError: # every probe may raise independently on a mixed collection
749
+ raised_counts[column] += 1
750
+ row.append(False)
751
+ satisfied.append(row)
752
+ assignment = _max_bipartite_assignment(satisfied)
753
+ unpaired_items = [index for index, column in enumerate(assignment) if column is None]
754
+ if unpaired_items:
755
+ paired_columns = {column for column in assignment if column is not None}
756
+ entries = [
757
+ _ROOT.member(items[index], "extra").entry(actual=items[index], expected=None, absent="expected")
758
+ for index in unpaired_items
759
+ ]
760
+ entries.extend(
761
+ DiffEntry(
762
+ path="missing",
763
+ actual=None,
764
+ absent="actual",
765
+ expected=_describe_unpaired(matcher, raised_counts[column]),
766
+ )
767
+ for column, matcher in enumerate(matchers)
768
+ if column not in paired_columns
769
+ )
770
+ failed = "item" if len(unpaired_items) == 1 else "items"
771
+ return self.error(
772
+ f"Expected items to satisfy the given matchers in any order,"
773
+ f" but no pairing covers {len(unpaired_items)} {failed}.",
774
+ actual=self.val,
775
+ expected=[_describe_matcher(matcher) for matcher in matchers],
776
+ diff=DiffResult(kind="contains", entries=entries),
777
+ )
778
+ return self
779
+
780
+ def zip_satisfies(
781
+ self, other: Iterable[object], predicate: Callable[..., bool], *, allow_empty: bool = False
782
+ ) -> Self:
783
+ """Asserts that each pair from zipping val with other satisfies the two-arg predicate.
784
+
785
+ Pairs the i-th item of val with the i-th item of ``other`` and checks ``predicate(a, b)``.
786
+ The two iterables must have equal length. Every failing pair is reported.
787
+
788
+ Args:
789
+ other: the iterable to zip with val
790
+ predicate: a two-arg callable returning a bool, applied to each ``(val_item, other_item)`` pair
791
+
792
+ Examples:
793
+ Usage:
794
+
795
+ assert_that([1, 2, 3]).zip_satisfies([2, 4, 6], lambda a, b: b == a * 2)
796
+ assert_that(["a", "bb"]).zip_satisfies([1, 2], lambda s, n: len(s) == n)
797
+
798
+ Returns:
799
+ AssertionBuilder: returns this instance to chain to the next assertion
800
+
801
+ Raises:
802
+ AssertionError: if the lengths differ, or any pair does **not** satisfy the predicate
803
+ TypeError: if val or other is not iterable, or predicate is not callable
804
+ """
805
+ if not callable(predicate):
806
+ refuse(predicate, "callable", subject=argument("predicate"))
807
+ if not isinstance(self.val, collections.abc.Iterable):
808
+ refuse(self.val, "iterable")
809
+ if not isinstance(other, collections.abc.Iterable):
810
+ refuse(other, "iterable", subject=argument("other"))
811
+ val_items = list(self._walked())
812
+ other_items = list(other)
813
+ if len(val_items) != len(other_items):
814
+ return self.error(
815
+ f"Expected collection length <{len(other_items)}>, but was <{len(val_items)}>.",
816
+ actual=self.val,
817
+ expected=other,
818
+ )
819
+ # warned here: a length mismatch is the verdict, and warning first said the call passed over nothing
820
+ if not val_items:
821
+ _warn_vacuous(self, "zip_satisfies", allow_empty)
822
+ entries = [
823
+ _ROOT.index(index).entry(actual=val_item, expected=other_item)
824
+ for index, (val_item, other_item) in enumerate(zip(val_items, other_items, strict=True))
825
+ if not verdict(predicate(val_item, other_item))
826
+ ]
827
+ if entries:
828
+ failed = "pair" if len(entries) == 1 else "pairs"
829
+ return self.error(
830
+ f"Expected paired items to satisfy <{predicate}>, but {len(entries)} {failed} did not.",
831
+ actual=self.val,
832
+ expected=other,
833
+ diff=DiffResult(kind="match", entries=entries),
834
+ )
835
+ return self