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/_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
|