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/contains.py ADDED
@@ -0,0 +1,1068 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter
4
+ from collections.abc import Iterable, Sequence
5
+ from typing import TYPE_CHECKING, Any, Final, cast
6
+
7
+ from ._engine._compare import _guarded_equal
8
+ from ._engine._diff import _sub_diff_entries
9
+ from ._engine._equality import fields_held, mapping_shaped
10
+ from ._engine._membership import (
11
+ _hash_safe,
12
+ _index,
13
+ has_duplicates,
14
+ is_searchable,
15
+ is_walkable,
16
+ missing_items,
17
+ occurrences,
18
+ only_faults,
19
+ repeated_counts,
20
+ searchable,
21
+ )
22
+ from ._engine._mixin_base import _MixinBase
23
+ from ._engine._ordering import REFUSALS, equals, lookup, may_broadcast, member
24
+ from ._engine._pairing import completed_pairing
25
+ from ._engine._path import _ROOT
26
+ from ._engine._require import argument, refuse, require_type, sized_len, verdict
27
+ from ._hints import Roles, not_found, not_in_text, reads_as, under
28
+ from .errors import DiffEntry, DiffResult, _capped, _capped_format, _capped_repr, _safe_repr, _told_apart
29
+ from .matchers import _is_matcher
30
+
31
+ if TYPE_CHECKING:
32
+ from ._engine._compat import Self
33
+
34
+ __tracebackhide__ = True
35
+
36
+
37
+ def _surplus(counted: Sequence[tuple[object, int]]) -> list[DiffEntry]:
38
+ """One `DiffEntry` per copy too many, in the shape every other containment failure uses.
39
+
40
+ A count paired against a value was the first shape, and the renderer showed nothing for it: only an
41
+ entry saying which side is missing something reaches the extra/missing groups. A repeat is a copy
42
+ the expectation has no room for, so it is spelled the same way an unwanted element is, and the
43
+ number of entries is how many copies over.
44
+ """
45
+ return [
46
+ DiffEntry(path="duplicated", actual=item, expected=None, absent="expected")
47
+ for item, total in counted
48
+ for _ in range(total - 1)
49
+ ]
50
+
51
+
52
+ def _counted_difference(val_items, given_items):
53
+ """``(extra, missing)`` by `Counter`, or ``None`` where hashing cannot stand in for ``==``.
54
+
55
+ A matcher hashes by identity and compares by its predicate, so counting them called a matching item
56
+ missing where the ordered spelling accepted it. The rule is membership's own.
57
+ """
58
+ if not (_hash_safe(val_items) and _hash_safe(given_items)):
59
+ return None
60
+ try:
61
+ val_counts, given_counts = Counter(val_items), Counter(given_items)
62
+ except TypeError: # a value refused to hash after all
63
+ return None
64
+ return list((val_counts - given_counts).elements()), list((given_counts - val_counts).elements())
65
+
66
+
67
+ def _sequence_break(values, items, *, answered=False) -> int | None:
68
+ """``None`` where *items* run contiguously in *values*, else how many lined up in the longest run.
69
+
70
+ Of the runs that start with room for all of *items*: one that starts too late to fit is `_late_run`'s.
71
+ Asked by ``==``, and *answered* by `equals` once a signalling NaN or an overflowing `numpy` float raised.
72
+ The very object is itself before either is asked, as it is to `in`: a NaN the value holds is found.
73
+ """
74
+ if not items:
75
+ return None
76
+ best_prefix = 0
77
+ first, rest = items[0], range(1, len(items))
78
+ for i in range(len(values) - len(items) + 1):
79
+ held = values[i]
80
+ # the start of a run asked on its own: through the loop below, a search of 200 for 3 cost 16% more
81
+ if not (held is first or (equals(held, first) if answered else held == first)):
82
+ continue
83
+ for j in rest:
84
+ held, wanted = values[i + j], items[j]
85
+ if held is wanted or (equals(held, wanted) if answered else held == wanted):
86
+ continue
87
+ best_prefix = max(best_prefix, j)
88
+ break
89
+ else:
90
+ return None
91
+ return best_prefix
92
+
93
+
94
+ def _late_run(values, items) -> int:
95
+ """How many items lined up in the longest run that starts too late in *values* to fit, for the message alone.
96
+
97
+ Left out, a value shorter than the sequence had started no run at all. Asked once the verdict is settled,
98
+ of elements the verdict never compared: one whose comparison raises costs the count and not the failure.
99
+ """
100
+ best = 0
101
+ try:
102
+ for i in range(max(len(values) - len(items) + 1, 0), len(values)):
103
+ j = 0
104
+ while i + j < len(values) and (values[i + j] is items[j] or equals(values[i + j], items[j])):
105
+ j += 1
106
+ best = max(best, j)
107
+ except Exception: # a diagnostic must never outrank the failure it is describing
108
+ return best
109
+ return best
110
+
111
+
112
+ def _walked_difference(val_items, given_items):
113
+ """``(extra, missing)`` through ``==``, as a maximum pairing of the items held with the items given.
114
+
115
+ A first-fit walk alone answered by the order the items were written in: against ``[1, 5]`` it gave
116
+ the 1 to ``match.greater_than(0)`` and found no 1 left for ``1``, though the 5 takes the matcher. It
117
+ still runs first, in the same order, and settles a pass alone. What it leaves unpaired is then
118
+ offered every held item a chain of displacements reaches, compared only as that chain asks, and
119
+ never about a pair the walk already asked: an ``==`` that answers once is not asked twice.
120
+ """
121
+ wanted = list(given_items)
122
+ free = list(range(len(wanted)))
123
+ paired: dict[int, int] = {}
124
+ refused: dict[int, set[int]] = {}
125
+ for index, item in enumerate(val_items):
126
+ for at, entry in enumerate(free):
127
+ if wanted[entry] is item or equals(wanted[entry], item):
128
+ paired[index] = free.pop(at)
129
+ break
130
+ refused.setdefault(entry, set()).add(index)
131
+ if free:
132
+ walked = dict(paired)
133
+ accepted: dict[int, list[int]] = {}
134
+
135
+ def holders(entry: int) -> list[int]:
136
+ if entry not in accepted:
137
+ one, asked = wanted[entry], refused.get(entry, set())
138
+ accepted[entry] = [
139
+ index
140
+ for index, item in enumerate(val_items)
141
+ if index not in asked and (walked.get(index) == entry or one is item or equals(one, item))
142
+ ]
143
+ return accepted[entry]
144
+
145
+ completed_pairing(paired, free, holders)
146
+ taken = set(paired.values())
147
+ return (
148
+ [item for index, item in enumerate(val_items) if index not in paired],
149
+ [one for entry, one in enumerate(wanted) if entry not in taken],
150
+ )
151
+
152
+
153
+ def _multiset_diff_entries(val_items, given_items):
154
+ """Build extra/missing `DiffEntry` rows between two item lists compared as multisets (order ignored)."""
155
+ extra, missing = _counted_difference(val_items, given_items) or _walked_difference(val_items, given_items)
156
+ entries = [
157
+ DiffEntry(path="extra", actual=item, expected=None, absent="expected") for item in sorted(extra, key=_safe_repr)
158
+ ]
159
+ entries.extend(
160
+ DiffEntry(path="missing", actual=None, absent="actual", expected=item)
161
+ for item in sorted(missing, key=_safe_repr)
162
+ )
163
+ return entries
164
+
165
+
166
+ def _as_row(value: object) -> Any:
167
+ """*value* as the keys or fields a closest element is looked for by, or ``None`` where it has neither."""
168
+ return value if mapping_shaped(value, check_values=False) else fields_held(value)
169
+
170
+
171
+ _OF_AN_ITEM: Final = Roles()
172
+ _OF_A_KEY: Final = Roles("the key not found", "a key", named=True)
173
+
174
+
175
+ def _why(item: object, values: Iterable[object], roles: Roles = _OF_AN_ITEM) -> str:
176
+ """The line that says why *item* was not found, on a line of its own under the sentence, or nothing."""
177
+ line = not_found(item, values, roles)
178
+ return "" if line is None else f"\n{line}"
179
+
180
+
181
+ def _is_held(item: object, values: object) -> bool:
182
+ """Whether *values* holds *item* after all, asked for a line of a failure: a search that raises says it may."""
183
+ try:
184
+ return member(item, values)
185
+ except Exception: # a diagnostic must never outrank the failure it is describing
186
+ return True
187
+
188
+
189
+ def _why_absent(item: object, values: Iterable[object]) -> str:
190
+ """`_why` for an item a failure names that the collection may still hold out of place: said where it does not."""
191
+ return "" if _is_held(item, values) else _why(item, values)
192
+
193
+
194
+ def _related(mine: object, other: object) -> bool:
195
+ """Whether two values under one key agree: equal, or two plain values of two types that read the same."""
196
+ if _guarded_equal(mine, other):
197
+ return True
198
+ return reads_as(mine, other)
199
+
200
+
201
+ def _closest(item, values, *, item_is_actual=False):
202
+ """The element of *values* most similar to *item*, with its diff entries, or ``None`` when nothing shares
203
+ enough structure to be an actionable 'did you mean' hint.
204
+
205
+ Asked of dict-like values and of records, a dataclass, an attrs instance, a named tuple or a model: a list
206
+ of rows is as often one as the other. Similarity is the fewest differing paths among elements that share
207
+ a key whose values are equal or read the same, so an element that shares no value is not offered and
208
+ ``{"id": 7}`` is offered for ``{"id": "7"}``. Runs only on a failed assertion, never on the hot path.
209
+
210
+ *item_is_actual* is which side of the entries the item is on: ``contains`` looks for what was expected among
211
+ what is held, ``is_in`` and a subset for what is held among what was given.
212
+
213
+ Reading a row runs its code, a field of a record or an item of a mapping, and what that raises costs
214
+ the hint and not the failure it was for.
215
+ """
216
+ try:
217
+ return _closest_row(item, values, item_is_actual)
218
+ except Exception: # a diagnostic must never outrank the failure it is describing
219
+ return None
220
+
221
+
222
+ def _closest_row(item, values, item_is_actual):
223
+ sought = _as_row(item)
224
+ if sought is None:
225
+ return None
226
+ best = None
227
+ for element in values:
228
+ held = _as_row(element)
229
+ if held is None:
230
+ continue
231
+ shared = ((held[key], *lookup(sought, key)) for key in held)
232
+ if not any(found and _related(mine, other) for mine, found, other in shared):
233
+ continue # no shared key that agrees -> not related enough to suggest
234
+ actual, expected = (item, element) if item_is_actual else (element, item)
235
+ entries = _sub_diff_entries(actual, expected, _ROOT, config=None)
236
+ # an element the walk finds nothing under, two records of a class with no equality, names no difference
237
+ if entries and (best is None or len(entries) < len(best[1])):
238
+ best = (element, entries)
239
+ return best
240
+
241
+
242
+ def _differences(entries, limit=3):
243
+ """A compact 'path (actual != expected)' summary of the closest element's differences."""
244
+ parts = ["{} ({} != {})".format(entry.path, *_two_sides(entry)) for entry in entries[:limit]]
245
+ if len(entries) > limit:
246
+ parts.append(f"and {len(entries) - limit} more")
247
+ return ", ".join(parts)
248
+
249
+
250
+ def _two_sides(entry: DiffEntry) -> tuple[str, str]:
251
+ """Both sides of one difference as text, a side that is not there named as such: its ``None`` read as a value."""
252
+ if entry.absent == "actual":
253
+ return "<missing>", _safe_repr(entry.expected)
254
+ if entry.absent == "expected":
255
+ return _safe_repr(entry.actual), "<missing>"
256
+ return _told_apart(_safe_repr(entry.actual), _safe_repr(entry.expected), entry.actual, entry.expected)
257
+
258
+
259
+ def _one_not_found(
260
+ missing: Sequence[object],
261
+ candidates: Iterable[object],
262
+ *,
263
+ noun: str | None = "element",
264
+ item_is_actual: bool = False,
265
+ roles: Roles = _OF_AN_ITEM,
266
+ searched: Iterable[object] | None = None,
267
+ ) -> str:
268
+ """What a membership failure says of the one item it did not find: the nearest row there is, and why not it.
269
+
270
+ Nothing for several items, each of which would need its own, and nothing for a matcher, which is no item.
271
+ *noun* is what the assertion calls a candidate, or ``None`` where a nearest one is not looked for. The line
272
+ is asked for last, past everything the sentence prints: printing runs code of the values. It looks among
273
+ *searched* where that is more than the candidates for the nearest row.
274
+ """
275
+ if len(missing) != 1 or _is_matcher(missing[0]):
276
+ return ""
277
+ item = missing[0]
278
+ nearest = ""
279
+ closest = None if noun is None else _closest(item, candidates, item_is_actual=item_is_actual)
280
+ if closest is not None:
281
+ element, entries = closest
282
+ nearest = f" Closest {noun} <{_capped_format(element)}> differs at {_differences(entries)}."
283
+ return nearest + _why(item, candidates if searched is None else searched, roles)
284
+
285
+
286
+ def _lone_pair(extra: Sequence[object], missing: Sequence[object]) -> tuple[str, str] | None:
287
+ """One element nobody asked for and one item not found, each as it is printed, told apart by class where the
288
+ two print the same. ``None`` for any other count: a list of several is printed as a list."""
289
+ if len(extra) != 1 or len(missing) != 1:
290
+ return None
291
+ shown = _told_apart(_capped_format(extra[0]), _capped_format(missing[0]), extra[0], missing[0])
292
+ return f"<{shown[0]}>", f"<{shown[1]}>"
293
+
294
+
295
+ def _unmatched(entries: Sequence[DiffEntry]) -> tuple[list[object], list[object]]:
296
+ """The ``(extra, missing)`` a multiset diff holds, as the values themselves."""
297
+ extra = [entry.actual for entry in entries if entry.absent == "expected"]
298
+ return extra, [entry.expected for entry in entries if entry.absent == "actual"]
299
+
300
+
301
+ def _both_ways(extra: Sequence[object], missing: Sequence[object], values: object, items: Sequence[object]) -> str:
302
+ """What is said where a collection was to hold the items given and nothing else.
303
+
304
+ Of the one item it lacks: its nearest row among the elements nobody asked for where there are any, which is
305
+ named as that, and the line among every element. Or, where it lacks none, of the one element nobody asked
306
+ for, against the items given.
307
+
308
+ Counted, a collection can be short of an item it has, or hold one it was asked for once too often: "not
309
+ found" and "not expected" would be false of it, so there the two counts are said instead (`_counted`).
310
+ """
311
+ held = cast("Iterable[object]", values)
312
+ if missing:
313
+ short = _the_one(missing)
314
+ if short is not _SEVERAL and _is_held(short, held):
315
+ return _counted(short, held, items, short=True)
316
+ if extra:
317
+ return _one_not_found(missing, extra, noun="unexpected element", searched=held)
318
+ return _one_not_found(missing, held)
319
+ over = _the_one(extra)
320
+ if over is not _SEVERAL and _is_held(over, items):
321
+ return _counted(over, held, items, short=False)
322
+ return _one_not_found(extra, items, noun=None, roles=Roles("the element not expected", "a given item"))
323
+
324
+
325
+ _SEVERAL: Final = object()
326
+
327
+
328
+ def _the_one(entries: Sequence[object]) -> object:
329
+ """The item every one of *entries* is, or `_SEVERAL` where they are of more than one, or there are none."""
330
+ try:
331
+ if entries and all(entry is entries[0] or equals(entry, entries[0]) for entry in entries[1:]):
332
+ return entries[0]
333
+ except Exception: # a diagnostic must never outrank the failure it is describing
334
+ return _SEVERAL
335
+ return _SEVERAL
336
+
337
+
338
+ def _counted(item: object, values: Iterable[object], items: Sequence[object], *, short: bool) -> str:
339
+ """How often a collection holds *item* against how often it was asked for, where it is *short* of it or over.
340
+
341
+ Counted again for the sentence, past the verdict and past every printing, that of the item included, all
342
+ of which run code of the values: said only where the two counts still show what the verdict found, held
343
+ fewer times than asked for or more.
344
+ """
345
+ shown = _capped_repr(item)
346
+ try:
347
+ held, asked = occurrences(list(values), [item])[0], occurrences(list(items), [item])[0]
348
+ except Exception: # a diagnostic must never outrank the failure it is describing
349
+ return ""
350
+ if not (0 < held < asked if short else held > asked > 0):
351
+ return ""
352
+ times = [f"{count} time{'' if count == 1 else 's'}" for count in (held, asked)]
353
+ return f" <{shown}> is held {times[0]} and was asked for {times[1]}."
354
+
355
+
356
+ def _value_not_found(mapping: Any, missing: Sequence[object]) -> str:
357
+ """What a failed ``contains_value`` says of the one value not found, among the values read again for it."""
358
+ try:
359
+ held = list(mapping.values())
360
+ except Exception: # a diagnostic must never outrank the failure it is describing
361
+ return ""
362
+ return _one_not_found(missing, held, noun="value", roles=Roles(held="a value"))
363
+
364
+
365
+ def _entry_not_found(mapping: object, missing: Sequence[dict[Any, Any]]) -> str:
366
+ """What a failed ``contains_entry`` says of the one entry not found: what its key holds, and why not it."""
367
+ if len(missing) != 1:
368
+ return ""
369
+ ((key, wanted),) = missing[0].items()
370
+ try:
371
+ found, held = lookup(mapping, key)
372
+ except Exception: # a diagnostic must never outrank the failure it is describing
373
+ return ""
374
+ if not found:
375
+ return f" There is no key <{_capped_format(key)}>." + _why(key, cast("Iterable[object]", mapping), _OF_A_KEY)
376
+ reason = _why(wanted, (held,), Roles("the value expected", "the value held"))
377
+ return f" Key <{_capped_format(key)}> holds <{_capped_repr(held)}>.{reason}"
378
+
379
+
380
+ def _pair_why(missing: Sequence[dict[Any, Any]], supersets: Sequence[dict[Any, Any]]) -> str:
381
+ """The line on why the one pair of a mapping no superset holds is not the pair a superset has under its key.
382
+
383
+ Nothing where no superset has the key: the pair is missing for that, whatever its value is.
384
+ """
385
+ if len(missing) != 1:
386
+ return ""
387
+ ((key, wanted),) = missing[0].items()
388
+ try:
389
+ held = [superset[key] for superset in supersets if key in superset]
390
+ except Exception: # a diagnostic must never outrank the failure it is describing
391
+ return ""
392
+ if not held:
393
+ return ""
394
+ return _why(wanted, held, Roles("the value missing", "the value the superset holds under the key"))
395
+
396
+
397
+ class ContainsMixin(_MixinBase):
398
+ """Containment assertions mixin."""
399
+
400
+ def contains(self, *items: object) -> Self:
401
+ """Asserts that val contains the given item or items.
402
+
403
+ Checks if the collection contains the given item or items using ``in`` operator.
404
+
405
+ Args:
406
+ *items: the item or items expected to be contained
407
+
408
+ Examples:
409
+ Usage:
410
+
411
+ assert_that('foo').contains('f')
412
+ assert_that('foo').contains('f', 'oo')
413
+ assert_that(['a', 'b']).contains('b', 'a')
414
+ assert_that((1, 2, 3)).contains(3, 2, 1)
415
+ assert_that({'a': 1, 'b': 2}).contains('b', 'a') # checks keys
416
+ assert_that({'a', 'b'}).contains('b', 'a')
417
+ assert_that([1, 2, 3]).is_type_of(list).contains(1, 2).does_not_contain(4, 5)
418
+
419
+ Returns:
420
+ AssertionBuilder: returns this instance to chain to the next assertion
421
+
422
+ Raises:
423
+ AssertionError: if val does **not** contain the item or items
424
+ TypeError: if val is not a container or iterable
425
+
426
+ Note:
427
+ Where one item is not found, the failure says what is nearest to it: the closest row among the elements,
428
+ and on a line under the message why it is not there, where one fact about it says so.
429
+
430
+ Tip:
431
+ Use the [`contains_key()`][assertpy.dict.DictMixin.contains_key] alias when working with
432
+ *dict-like* objects to be self-documenting.
433
+
434
+ See Also:
435
+ [`contains_ignoring_case()`][assertpy.string.StringMixin.contains_ignoring_case] -
436
+ for case-insensitive string contains
437
+ """
438
+ if len(items) == 0:
439
+ raise ValueError("one or more args must be given")
440
+ # membership is tested once per argument, so a one-shot iterator has to be drained first
441
+ values = searchable(self._drained())
442
+ if not is_searchable(values):
443
+ # left to `in`, Python answers about the operator rather than about the value the assertion was handed
444
+ refuse(self.val, "a container or iterable")
445
+ if len(items) == 1:
446
+ item = items[0]
447
+ if _is_matcher(item):
448
+ if not any(verdict(item.matches(value), subject="the matcher") for value in values):
449
+ diff = DiffResult(
450
+ kind="contains",
451
+ entries=[DiffEntry(path="missing", actual=None, absent="actual", expected=item.describe())],
452
+ )
453
+ return self.error(
454
+ f"Expected <{_capped_format(values)}> to contain item matching {item.describe()}, but did not.",
455
+ diff=diff,
456
+ expected=items,
457
+ )
458
+ elif not member(item, values):
459
+ if mapping_shaped(values):
460
+ diff = DiffResult(
461
+ kind="contains",
462
+ entries=[DiffEntry(path="missing", actual=None, absent="actual", expected=item)],
463
+ )
464
+ return self.error(
465
+ f"Expected <{_capped_format(values)}> to contain key <{_capped_format(item)}>, but did not."
466
+ f"{_why(item, values, _OF_A_KEY)}",
467
+ diff=diff,
468
+ expected=items,
469
+ )
470
+ closest = _closest(item, values)
471
+ if closest is not None:
472
+ element, entries = closest
473
+ return self.error(
474
+ f"Expected <{_capped_format(values)}> to contain item <{_capped_format(item)}>, but did not."
475
+ f" Closest element <{_capped_format(element)}> differs at {_differences(entries)}."
476
+ f"{_why(item, values)}",
477
+ diff=DiffResult(kind="contains", entries=entries),
478
+ expected=items,
479
+ )
480
+ diff = DiffResult(
481
+ kind="contains", entries=[DiffEntry(path="missing", actual=None, absent="actual", expected=item)]
482
+ )
483
+ in_text = type(values) is str and type(item) is str
484
+ return self.error(
485
+ f"Expected <{_capped_format(values)}> to contain item <{_capped_format(item)}>, but did not."
486
+ f"{under(not_in_text(values, item)) if in_text else _why(item, values)}",
487
+ diff=diff,
488
+ expected=items,
489
+ )
490
+ else:
491
+ missing = missing_items(values, items, _is_matcher)
492
+ if missing:
493
+ missing_desc = [
494
+ missing_item.describe() if _is_matcher(missing_item) else missing_item for missing_item in missing
495
+ ]
496
+ diff = DiffResult(
497
+ kind="contains",
498
+ entries=[
499
+ DiffEntry(path="missing", actual=None, absent="actual", expected=missing_item)
500
+ for missing_item in missing_desc
501
+ ],
502
+ )
503
+ if mapping_shaped(values):
504
+ sentence = (
505
+ f"Expected <{_capped_format(values)}> to contain keys {self._fmt_items(items)},"
506
+ f" but did not contain key{'' if len(missing) == 1 else 's'} {self._fmt_items(missing_desc)}."
507
+ )
508
+ said = _one_not_found(missing, values, noun=None, roles=_OF_A_KEY)
509
+ else:
510
+ sentence = (
511
+ f"Expected <{_capped_format(values)}> to contain items {self._fmt_items(items)},"
512
+ f" but did not contain {self._fmt_items(missing_desc)}."
513
+ )
514
+ said = _one_not_found(missing, values)
515
+ return self.error(sentence + said, diff=diff, expected=items)
516
+ return self
517
+
518
+ def does_not_contain(self, *items: object) -> Self:
519
+ """Asserts that val does not contain the given item or items.
520
+
521
+ Checks if the collection excludes the given item or items using ``in`` operator.
522
+
523
+ Args:
524
+ *items: the item or items expected to be excluded
525
+
526
+ Examples:
527
+ Usage:
528
+
529
+ assert_that('foo').does_not_contain('x')
530
+ assert_that(['a', 'b']).does_not_contain('x', 'y')
531
+ assert_that((1, 2, 3)).does_not_contain(4, 5)
532
+ assert_that({'a': 1, 'b': 2}).does_not_contain('x', 'y') # checks keys
533
+ assert_that({'a', 'b'}).does_not_contain('x', 'y')
534
+ assert_that([1, 2, 3]).is_type_of(list).contains(1, 2).does_not_contain(4, 5)
535
+
536
+ Returns:
537
+ AssertionBuilder: returns this instance to chain to the next assertion
538
+
539
+ Raises:
540
+ AssertionError: if val **does** contain the item or items
541
+ TypeError: if val is not a container or iterable
542
+
543
+ Note:
544
+ Accepts a `Matcher` for any item, the same as
545
+ [`contains()`][assertpy.contains.ContainsMixin.contains]: the assertion then fails when any
546
+ item of val matches it.
547
+
548
+ Tip:
549
+ Use the [`does_not_contain_key()`][assertpy.dict.DictMixin.does_not_contain_key] alias when working with
550
+ *dict-like* objects to be self-documenting.
551
+ """
552
+ if len(items) == 0:
553
+ raise ValueError("one or more args must be given")
554
+ values = self._drained()
555
+ if not is_searchable(values):
556
+ refuse(self.val, "a container or iterable")
557
+ probes = [item for item in items if not _is_matcher(item)]
558
+ # the index `contains` already builds for the same question: asked one item at a time, each of
559
+ # them walks the whole collection
560
+ searched = _index(values, probes) if len(probes) > 1 else None
561
+ lookup = values if searched is None else searched
562
+
563
+ def described(item: object) -> object:
564
+ return item.describe() if _is_matcher(item) else item
565
+
566
+ def present(item: object) -> bool:
567
+ # a matcher handed here was compared with `in`, which asks the wrong question
568
+ if _is_matcher(item):
569
+ return any(verdict(item.matches(value), subject="the matcher") for value in values)
570
+ return member(item, lookup)
571
+
572
+ if len(items) == 1:
573
+ if present(items[0]):
574
+ return self.error(
575
+ f"Expected <{_capped_format(values)}> to not contain item"
576
+ f" <{_capped_format(described(items[0]))}>, but did."
577
+ )
578
+ else:
579
+ found = [item for item in items if present(item)]
580
+ if found:
581
+ shown = [described(item) for item in items]
582
+ found_shown = [described(item) for item in found]
583
+ return self.error(
584
+ f"Expected <{_capped_format(values)}> to not contain items {self._fmt_items(shown)},"
585
+ f" but did contain {self._fmt_items(found_shown)}."
586
+ )
587
+ return self
588
+
589
+ def contains_only(self, *items: object) -> Self:
590
+ """Asserts that val contains *only* the given item or items.
591
+
592
+ Checks if the collection contains only the given item or items using ``in`` operator.
593
+
594
+ Args:
595
+ *items: the *only* item or items expected to be contained
596
+
597
+ Examples:
598
+ Usage:
599
+
600
+ assert_that('foo').contains_only('f', 'o')
601
+ assert_that(['a', 'a', 'b']).contains_only('a', 'b')
602
+ assert_that((1, 1, 2)).contains_only(1, 2)
603
+ assert_that({'a': 1, 'a': 2, 'b': 3}).contains_only('a', 'b')
604
+ assert_that({'a', 'a', 'b'}).contains_only('a', 'b')
605
+
606
+ Returns:
607
+ AssertionBuilder: returns this instance to chain to the next assertion
608
+
609
+ Raises:
610
+ AssertionError: if val contains anything **not** item or items
611
+
612
+ Note:
613
+ Where one item is not found, the failure says what is nearest to it: the closest row among the
614
+ elements nobody asked for, and on a line under the message why it is not there, where one fact about
615
+ it says so.
616
+ """
617
+ if len(items) == 0:
618
+ raise ValueError("one or more args must be given")
619
+ # walked twice below and rendered a third time, so a one-shot iterator has to be drained
620
+ values = searchable(self._drained())
621
+ if not is_walkable(values):
622
+ # "only these" has to see every element, and the comprehension answered "object is not iterable"
623
+ refuse(self.val, "iterable")
624
+ extra, missing = only_faults(values, items)
625
+ if extra or missing:
626
+ # both halves at once: reporting only the extras sends the reader to rerun into the other
627
+ faults = []
628
+ entries = []
629
+ had, lacked = _lone_pair(extra, missing) or (self._fmt_items(extra), self._fmt_items(missing))
630
+ if extra:
631
+ faults.append(f"did contain {had}")
632
+ entries += [DiffEntry(path="extra", actual=item, expected=None, absent="expected") for item in extra]
633
+ if missing:
634
+ faults.append(f"did not contain {lacked}")
635
+ entries += [DiffEntry(path="missing", actual=None, absent="actual", expected=item) for item in missing]
636
+ return self.error(
637
+ f"Expected <{_capped_format(values)}> to contain only {self._fmt_items(items)},"
638
+ f" but {' and '.join(faults)}." + _both_ways(extra, missing, values, items),
639
+ diff=DiffResult(kind="contains", entries=entries),
640
+ expected=items,
641
+ )
642
+ return self
643
+
644
+ def contains_sequence(self, *items: object) -> Self:
645
+ """Asserts that val contains the given ordered sequence of items.
646
+
647
+ Checks if the collection contains the given sequence of items using ``in`` operator.
648
+
649
+ Args:
650
+ *items: the sequence of items expected to be contained
651
+
652
+ Examples:
653
+ Usage:
654
+
655
+ assert_that('foo').contains_sequence('f', 'o')
656
+ assert_that('foo').contains_sequence('o', 'o')
657
+ assert_that(['a', 'b', 'c']).contains_sequence('b', 'c')
658
+ assert_that((1, 2, 3)).contains_sequence(1, 2)
659
+
660
+ Returns:
661
+ AssertionBuilder: returns this instance to chain to the next assertion
662
+
663
+ Raises:
664
+ AssertionError: if val does **not** contains the given sequence of items
665
+ """
666
+ if len(items) == 0:
667
+ raise ValueError("one or more args must be given")
668
+ if isinstance(self.val, str):
669
+ search_start = 0
670
+ for item in items:
671
+ text = require_type(item, str, "a string, to match val", subject=argument("item"))
672
+ found_index = self.val.find(text, search_start)
673
+ if found_index == -1:
674
+ # name where the chain broke: "but did not" makes the reader re-derive it by eye
675
+ matched = items[: items.index(item)]
676
+ trail = f" after {self._fmt_items(matched)}" if matched else ""
677
+ return self.error(
678
+ f"Expected <{_capped(self.val)}> to contain sequence {self._fmt_items(items)},"
679
+ f" but <{_capped_format(item)}> was not found{trail}.",
680
+ expected=items,
681
+ )
682
+ search_start = found_index + len(text)
683
+ return self
684
+ # this walk is by index, which a one-shot iterator does not support at all
685
+ values = self._drained()
686
+ if not isinstance(values, Sequence):
687
+ # the old guard said "not iterable" for both: true for an int, false for a set, which has no order
688
+ require_type(values, Iterable, "iterable")
689
+ refuse(self.val, "a sequence, to contain a sequence")
690
+ try:
691
+ best_prefix = _sequence_break(values, items, answered=any(may_broadcast(item) for item in items))
692
+ except REFUSALS:
693
+ best_prefix = _sequence_break(values, items, answered=True)
694
+ if best_prefix is None:
695
+ return self
696
+ best_prefix = max(best_prefix, _late_run(values, items))
697
+ # the longest run that lined up says where the sequence broke down
698
+ detail = (
699
+ f" The longest run that matched was {self._fmt_items(items[:best_prefix])}."
700
+ if best_prefix
701
+ # X may well be present, just never at a position where the whole sequence still fits
702
+ else f" No run started with <{_capped_format(items[0])}>."
703
+ )
704
+ return self.error(
705
+ f"Expected <{_capped_format(values)}> to contain sequence {self._fmt_items(items)}, but did not.{detail}"
706
+ + _why_absent(items[best_prefix], values),
707
+ expected=items,
708
+ )
709
+
710
+ def contains_duplicates(self) -> Self:
711
+ """Asserts that val is iterable and *does* contain duplicates.
712
+
713
+ Examples:
714
+ Usage:
715
+
716
+ assert_that('foo').contains_duplicates()
717
+ assert_that(['a', 'a', 'b']).contains_duplicates()
718
+ assert_that((1, 1, 2)).contains_duplicates()
719
+
720
+ Returns:
721
+ AssertionBuilder: returns this instance to chain to the next assertion
722
+
723
+ Raises:
724
+ AssertionError: if val does **not** contain any duplicates
725
+ """
726
+ try:
727
+ values = list(self._walked())
728
+ except TypeError:
729
+ refuse(self.val, "iterable")
730
+ if has_duplicates(values):
731
+ return self
732
+ return self.error(f"Expected <{_capped(self.val)}> to contain duplicates, but did not.")
733
+
734
+ def does_not_contain_duplicates(self) -> Self:
735
+ """Asserts that val is iterable and *does not* contain any duplicates.
736
+
737
+ Examples:
738
+ Usage:
739
+
740
+ assert_that('fox').does_not_contain_duplicates()
741
+ assert_that(['a', 'b', 'c']).does_not_contain_duplicates()
742
+ assert_that((1, 2, 3)).does_not_contain_duplicates()
743
+
744
+ Returns:
745
+ AssertionBuilder: returns this instance to chain to the next assertion
746
+
747
+ Raises:
748
+ AssertionError: if val **does** contain duplicates
749
+ """
750
+ try:
751
+ values = list(self._walked())
752
+ except TypeError:
753
+ refuse(self.val, "iterable")
754
+ if not has_duplicates(values):
755
+ return self
756
+ # "but did" leaves the reader to scan the value for the repeat
757
+ repeated = repeated_counts(values)
758
+ named = [value for value, _total in repeated]
759
+ return self.error(
760
+ f"Expected <{_capped(self.val)}> to not contain duplicates, but {self._fmt_items(named)}"
761
+ f" {'was' if len(named) == 1 else 'were'} repeated.",
762
+ diff=DiffResult(kind="contains", entries=_surplus(repeated)),
763
+ )
764
+
765
+ def is_empty(self) -> Self:
766
+ """Asserts that val is empty.
767
+
768
+ Examples:
769
+ Usage:
770
+
771
+ assert_that('').is_empty()
772
+ assert_that([]).is_empty()
773
+ assert_that(()).is_empty()
774
+ assert_that({}).is_empty()
775
+ assert_that(set()).is_empty()
776
+
777
+ Returns:
778
+ AssertionBuilder: returns this instance to chain to the next assertion
779
+
780
+ Raises:
781
+ AssertionError: if val is **not** empty
782
+ """
783
+ if sized_len(self.val) != 0:
784
+ if isinstance(self.val, str):
785
+ return self.error(f"Expected <{_capped(self.val)}> to be empty string, but was not.")
786
+ else:
787
+ return self.error(f"Expected <{_capped(self.val)}> to be empty, but was not.")
788
+ return self
789
+
790
+ def is_not_empty(self) -> Self:
791
+ """Asserts that val is *not* empty.
792
+
793
+ Examples:
794
+ Usage:
795
+
796
+ assert_that('foo').is_not_empty()
797
+ assert_that(['a', 'b']).is_not_empty()
798
+ assert_that((1, 2, 3)).is_not_empty()
799
+ assert_that({'a': 1, 'b': 2}).is_not_empty()
800
+ assert_that({'a', 'b'}).is_not_empty()
801
+
802
+ Returns:
803
+ AssertionBuilder: returns this instance to chain to the next assertion
804
+
805
+ Raises:
806
+ AssertionError: if val **is** empty
807
+ """
808
+ if sized_len(self.val) == 0:
809
+ if isinstance(self.val, str):
810
+ return self.error("Expected not empty string, but was empty.")
811
+ else:
812
+ return self.error("Expected not empty, but was empty.")
813
+ return self
814
+
815
+ def contains_exactly(self, *items: object) -> Self:
816
+ """Asserts that val contains exactly the given items in the given order.
817
+
818
+ Unlike [`contains_only()`][assertpy.contains.ContainsMixin.contains_only] (which ignores
819
+ order) and [`contains_sequence()`][assertpy.contains.ContainsMixin.contains_sequence]
820
+ (which allows extra items), this method requires exact count, items, and order.
821
+
822
+ Args:
823
+ *items: the items expected, in exact order
824
+
825
+ Examples:
826
+ Usage:
827
+
828
+ assert_that([1, 2, 3]).contains_exactly(1, 2, 3)
829
+ assert_that(['a', 'b']).contains_exactly('a', 'b')
830
+
831
+ Returns:
832
+ AssertionBuilder: returns this instance to chain to the next assertion
833
+
834
+ Raises:
835
+ AssertionError: if val does **not** contain exactly the given items in order
836
+ """
837
+ if len(items) == 0:
838
+ raise ValueError("one or more args must be given")
839
+ try:
840
+ val_list = list(self._walked())
841
+ except TypeError:
842
+ refuse(self.val, "iterable")
843
+ expected_list = list(items)
844
+ if equals(val_list, expected_list):
845
+ return self
846
+ message = f"Expected <{_capped(self.val)}> to contain exactly {self._fmt_items(items)}, but did not."
847
+ entries = _multiset_diff_entries(val_list, expected_list)
848
+ # equal multisets, so only the order differs: name the first position that disagrees
849
+ disagreeing = (
850
+ i
851
+ for i, (found, wanted) in enumerate(zip(val_list, expected_list, strict=True))
852
+ if not _guarded_equal(found, wanted, method="contains_exactly")
853
+ )
854
+ # none, from an `__eq__` that answered the list's question and the rescan's differently
855
+ index = None if entries else next(disagreeing, None)
856
+ if index is None:
857
+ diff = DiffResult(kind="contains", entries=entries)
858
+ message += _both_ways(*_unmatched(entries), val_list, expected_list)
859
+ else:
860
+ message += f" Same items, but the order differs at index {index}."
861
+ diff = DiffResult(
862
+ kind="sequence",
863
+ entries=[_ROOT.index(index).entry(actual=val_list[index], expected=expected_list[index])],
864
+ )
865
+ return self.error(message, diff=diff, expected=items)
866
+
867
+ def contains_exactly_in_any_order(self, *items: object) -> Self:
868
+ """Asserts that val contains exactly the given items, in any order.
869
+
870
+ Like [`contains_exactly()`][assertpy.contains.ContainsMixin.contains_exactly] but ignoring
871
+ order: val and the given items must be equal as multisets, so duplicates count (each item
872
+ must occur exactly as many times as given). Unlike
873
+ [`contains_only()`][assertpy.contains.ContainsMixin.contains_only] (which checks membership
874
+ both ways and ignores counts), an extra duplicate or a missing one fails.
875
+
876
+ Args:
877
+ *items: the items expected, in any order
878
+
879
+ Examples:
880
+ Usage:
881
+
882
+ assert_that([3, 1, 2]).contains_exactly_in_any_order(1, 2, 3)
883
+ assert_that(['b', 'a', 'b']).contains_exactly_in_any_order('a', 'b', 'b')
884
+
885
+ assert_that([1, 2, 2]).contains_exactly_in_any_order(1, 2) # fails (extra 2)
886
+ assert_that([1, 2]).contains_exactly_in_any_order(1, 2, 2) # fails (missing 2)
887
+
888
+ Returns:
889
+ AssertionBuilder: returns this instance to chain to the next assertion
890
+
891
+ Raises:
892
+ AssertionError: if val does **not** contain exactly the given items in any order
893
+ TypeError: if val is not iterable
894
+ ValueError: if no items are given
895
+ """
896
+ if len(items) == 0:
897
+ raise ValueError("one or more args must be given")
898
+ try:
899
+ val_list = list(self._walked())
900
+ except TypeError:
901
+ refuse(self.val, "iterable")
902
+ entries = _multiset_diff_entries(val_list, list(items))
903
+ if entries:
904
+ return self.error(
905
+ f"Expected <{_capped(self.val)}> to contain exactly {self._fmt_items(items)} in any order, "
906
+ f"but did not." + _both_ways(*_unmatched(entries), val_list, items),
907
+ diff=DiffResult(kind="contains", entries=entries),
908
+ expected=items,
909
+ )
910
+ return self
911
+
912
+ def contains_in_order(self, *items: object) -> Self:
913
+ """Asserts that val contains the given items in the given order (as a subsequence).
914
+
915
+ Items must appear in the given order but do not need to be contiguous.
916
+ Unlike [`contains_sequence()`][assertpy.contains.ContainsMixin.contains_sequence] which
917
+ requires contiguous items.
918
+
919
+ Args:
920
+ *items: the items expected, in order (but not necessarily contiguous)
921
+
922
+ Examples:
923
+ Usage:
924
+
925
+ assert_that([1, 5, 2, 8, 3]).contains_in_order(1, 2, 3)
926
+ assert_that(['a', 'x', 'b', 'y', 'c']).contains_in_order('a', 'b', 'c')
927
+
928
+ Returns:
929
+ AssertionBuilder: returns this instance to chain to the next assertion
930
+
931
+ Raises:
932
+ AssertionError: if val does **not** contain items in the given order
933
+ """
934
+ if len(items) == 0:
935
+ raise ValueError("one or more args must be given")
936
+ try:
937
+ val_list = list(self._walked())
938
+ except TypeError:
939
+ refuse(self.val, "iterable")
940
+ item_index, wanted = 0, items[0]
941
+ for element in val_list:
942
+ # the very object is itself before `==` is asked, as it is to `in`: a NaN the value holds follows
943
+ if element is wanted or equals(element, wanted):
944
+ item_index += 1
945
+ if item_index == len(items):
946
+ break
947
+ wanted = items[item_index]
948
+ if item_index != len(items):
949
+ # item_index counts how many lined up before the run stopped, so the next one is the culprit
950
+ matched = items[:item_index]
951
+ trail = f" after {self._fmt_items(matched)}" if matched else ""
952
+ return self.error(
953
+ f"Expected <{_capped(self.val)}> to contain {self._fmt_items(items)} in order, "
954
+ f"but <{_capped_format(items[item_index])}> did not follow{trail}."
955
+ + _why_absent(items[item_index], val_list),
956
+ expected=items,
957
+ )
958
+ return self
959
+
960
+ def contains_only_once(self, *items: object) -> Self:
961
+ """Asserts that val contains each given item exactly once.
962
+
963
+ Each given item must appear in val with a count of exactly one: an item absent from val is
964
+ reported as missing, an item occurring more than once is reported as duplicated.
965
+
966
+ Args:
967
+ *items: the items each expected to occur exactly once
968
+
969
+ Examples:
970
+ Usage:
971
+
972
+ assert_that([1, 2, 3]).contains_only_once(1, 3)
973
+ assert_that('foo').contains_only_once('f')
974
+
975
+ assert_that([1, 2, 2, 3]).contains_only_once(2) # fails (occurs twice)
976
+ assert_that([1, 2, 3]).contains_only_once(4) # fails (missing)
977
+
978
+ Returns:
979
+ AssertionBuilder: returns this instance to chain to the next assertion
980
+
981
+ Raises:
982
+ AssertionError: if any given item is missing from val or occurs more than once
983
+ TypeError: if val is not iterable
984
+ ValueError: if no items are given
985
+ """
986
+ if len(items) == 0:
987
+ raise ValueError("one or more args must be given")
988
+ try:
989
+ val_list = list(self._walked())
990
+ except TypeError:
991
+ refuse(self.val, "iterable")
992
+ counts = occurrences(val_list, items)
993
+ missing = [item for item, total in zip(items, counts, strict=True) if total == 0]
994
+ repeats = [(item, total) for item, total in zip(items, counts, strict=True) if total > 1]
995
+ duplicated = [item for item, _ in repeats]
996
+ if missing or duplicated:
997
+ entries = [DiffEntry(path="missing", actual=None, absent="actual", expected=item) for item in missing]
998
+ entries.extend(_surplus(repeats))
999
+ problems = []
1000
+ if missing:
1001
+ problems.append(f"did not contain {self._fmt_items(missing)}")
1002
+ if duplicated:
1003
+ problems.append(f"contained {self._fmt_items(duplicated)} more than once")
1004
+ return self.error(
1005
+ f"Expected <{_capped_format(val_list)}> to contain {self._fmt_items(items)} only once,"
1006
+ f" but {' and '.join(problems)}." + _one_not_found(missing, val_list),
1007
+ diff=DiffResult(kind="contains", entries=entries),
1008
+ expected=items,
1009
+ )
1010
+ return self
1011
+
1012
+ def is_in(self, *items: object) -> Self:
1013
+ """Asserts that val is equal to one of the given items.
1014
+
1015
+ Args:
1016
+ *items: the items expected to contain val
1017
+
1018
+ Examples:
1019
+ Usage:
1020
+
1021
+ assert_that('foo').is_in('foo', 'bar', 'baz')
1022
+ assert_that(1).is_in(0, 1, 2, 3)
1023
+
1024
+ Returns:
1025
+ AssertionBuilder: returns this instance to chain to the next assertion
1026
+
1027
+ Raises:
1028
+ AssertionError: if val is **not** in the given items
1029
+
1030
+ Note:
1031
+ The failure says what is nearest to val: the closest row among the given items, and on a line under
1032
+ the message why val is in none of them, where one fact about it says so.
1033
+ """
1034
+ if len(items) == 0:
1035
+ raise ValueError("one or more args must be given")
1036
+ # identity first, as `in` asks: the very NaN a tuple holds is in it, and `==` alone said no
1037
+ if member(self.val, items):
1038
+ return self
1039
+ roles = Roles("the value", "a given item")
1040
+ return self.error(
1041
+ f"Expected <{_capped(self.val)}> to be in {self._fmt_items(items)}, but was not."
1042
+ + _one_not_found((self.val,), items, noun="item", item_is_actual=True, roles=roles),
1043
+ expected=items,
1044
+ )
1045
+
1046
+ def is_not_in(self, *items: object) -> Self:
1047
+ """Asserts that val is not equal to one of the given items.
1048
+
1049
+ Args:
1050
+ *items: the items expected to exclude val
1051
+
1052
+ Examples:
1053
+ Usage:
1054
+
1055
+ assert_that('foo').is_not_in('bar', 'baz', 'box')
1056
+ assert_that(1).is_not_in(-1, -2, -3)
1057
+
1058
+ Returns:
1059
+ AssertionBuilder: returns this instance to chain to the next assertion
1060
+
1061
+ Raises:
1062
+ AssertionError: if val **is** in the given items
1063
+ """
1064
+ if len(items) == 0:
1065
+ raise ValueError("one or more args must be given")
1066
+ if member(self.val, items):
1067
+ return self.error(f"Expected <{_capped(self.val)}> to not be in {self._fmt_items(items)}, but was.")
1068
+ return self