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/base.py ADDED
@@ -0,0 +1,862 @@
1
+ from __future__ import annotations
2
+
3
+ import decimal
4
+ from types import GenericAlias
5
+ from typing import TYPE_CHECKING, SupportsIndex
6
+
7
+ from ._engine._compare import (
8
+ _EQ_ATOMIC,
9
+ _ambiguous_array_operand,
10
+ _array_equality_error,
11
+ _build_compare_config,
12
+ _config_note,
13
+ _guarded_equal,
14
+ _kinds_never_equal,
15
+ _node_decision,
16
+ _resolve_comparator,
17
+ _types_differ,
18
+ )
19
+ from ._engine._diff import _build_equality_diff, _child_entries
20
+ from ._engine._equality import (
21
+ fields_pair,
22
+ filtered_to_nothing,
23
+ key_specs_given,
24
+ mapping_shaped,
25
+ )
26
+ from ._engine._introspection import is_namedtuple
27
+ from ._engine._ordering import require_integer
28
+ from ._engine._path import _ROOT
29
+ from ._engine._require import argument, refuse, reject_unknown_kwargs, require_type, sized_len, verdict
30
+ from ._hints import identity_candidate
31
+ from ._matcher_impls import _is_matcher, _outcome_of, _ReadOnce, _require_class_info
32
+ from ._satisfies import SatisfiesMixin, _guarding, _warn_nothing_compared
33
+ from .errors import _capped, _capped_format, _disambiguated, _safe_str, _told_apart, _truncated, _type_expression_name
34
+ from .helpers import _both_list_like, _elided_seq_repr, _elided_text_repr
35
+
36
+ if TYPE_CHECKING:
37
+ from ._engine._compare import _CompareConfig
38
+ from ._engine._compat import Self
39
+ from ._matcher_impls import ClassInfo, Matcher
40
+
41
+ __tracebackhide__ = True
42
+
43
+ _IS_EQUAL_TO_OPTIONS = frozenset(
44
+ {"ignore", "include", "tolerance", "rel", "comparators", "ignore_null", "strict_types"}
45
+ )
46
+
47
+
48
+ class BaseMixin(SatisfiesMixin):
49
+ """Base mixin."""
50
+
51
+ def described_as(self, description: str) -> Self:
52
+ """Describes the assertion. On failure, the description is included in the error message.
53
+
54
+ This is not an assertion itself. But if the any of the following chained assertions fail,
55
+ the description will be included in addition to the regular error message.
56
+
57
+ Args:
58
+ description: the error message description
59
+
60
+ Examples:
61
+ Usage:
62
+
63
+ assert_that(1).described_as('error msg desc').is_equal_to(2) # fails
64
+ # [error msg desc] Expected <1> to be equal to <2>, but was not.
65
+
66
+ Returns:
67
+ AssertionBuilder: returns this instance to chain to the next assertion
68
+ """
69
+ self.description: str = str(description)
70
+ return self
71
+
72
+ def is_equal_to(self, other: object, **kwargs: object) -> Self:
73
+ """Asserts that val is equal to other.
74
+
75
+ Checks actual is equal to expected using the ``==`` operator. When val is *dict-like*
76
+ or has introspectable fields (dataclass, namedtuple, attrs, Pydantic model),
77
+ optionally ignore or include keys/fields when checking equality.
78
+
79
+ Args:
80
+ other: the expected value
81
+ **kwargs: see below
82
+
83
+ Keyword Args:
84
+ ignore (Hashable | list | set | frozenset | None): the key/field (or list/set/frozenset of
85
+ keys/fields) to ignore. Besides exact keys and nested-path tuples, a ``re.Pattern`` matches
86
+ field names by regex and a ``type`` matches fields by value type.
87
+ include (Hashable | list | set | frozenset | None): the key/field (or list/set/frozenset of
88
+ keys/fields) to include. Accepts the same ``re.Pattern`` / ``type`` specs as ``ignore``. A
89
+ key the value does not have fails the assertion with or without ``not_``.
90
+ tolerance (float | None): an absolute tolerance that widens ``==`` for every pair of real-number
91
+ leaves anywhere in the structure: a pair ``==`` holds equal stays equal, and any other is
92
+ measured as [`is_close_to()`][assertpy.numeric.NumericMixin.is_close_to] measures it.
93
+ comparators (dict | None): a dict mapping a ``type`` or a field name to an
94
+ ``(actual, expected) -> bool`` predicate that alone decides every node it matches, the root
95
+ included, even inside a container whose ``==`` holds; a field-name key wins over a type
96
+ key. Mapping keys, set members and whatever a container shared by both sides holds are
97
+ left to ``==``.
98
+ ignore_null (bool): when ``True``, skip any named field the *expected* side leaves ``None``
99
+ (a partial expected/template), at any depth. Only the expected side is skipped, so an
100
+ unexpectedly ``None`` actual field is still reported. Defaults to ``False``.
101
+ strict_types (bool): when ``True``, both sides of every node must be the same type, at any
102
+ depth. Plain ``==`` does not require this: ``True == 1``, ``Decimal("1") == 1`` and
103
+ ``[True] == [1]`` are all true, so a boolean read from JSON compares equal to an
104
+ integer without a word. Opting in also rejects pairs some callers consider equal
105
+ (``IntEnum`` against ``int``, a ``dict`` subclass against ``dict``, ``float`` against
106
+ ``int``), and it wins over ``tolerance``, which says how far apart two numbers may be
107
+ and not that they may be different types. A ``comparators`` entry still decides the
108
+ nodes it matches ahead of it, the root included, and a `Matcher` on the expected side is
109
+ exempt, so composed matchers keep working. Dictionary keys and set elements are covered
110
+ too, although a container matches them by hash before any type is looked at: ``{True: "a"}`` against
111
+ ``{1: "a"}`` and ``{1}`` against ``{1.0}`` both fail, as ``[True]`` against ``[1]``
112
+ does. Defaults to ``False``.
113
+
114
+ Examples:
115
+ Usage:
116
+
117
+ assert_that(1 + 2).is_equal_to(3)
118
+ assert_that('foo').is_equal_to('foo')
119
+ assert_that(123).is_equal_to(123)
120
+ assert_that(123.4).is_equal_to(123.4)
121
+ assert_that(['a', 'b']).is_equal_to(['a', 'b'])
122
+ assert_that((1, 2, 3)).is_equal_to((1, 2, 3))
123
+ assert_that({'a': 1, 'b': 2}).is_equal_to({'a': 1, 'b': 2})
124
+ assert_that({'a', 'b'}).is_equal_to({'a', 'b'})
125
+
126
+ When the val is *dict-like*, keys can optionally be *ignored* when checking equality:
127
+
128
+ # ignore a single key
129
+ assert_that({'a': 1, 'b': 2}).is_equal_to({'a': 1}, ignore='b')
130
+
131
+ # ignore multiple keys
132
+ assert_that({'a': 1, 'b': 2, 'c': 3}).is_equal_to({'a': 1}, ignore=['b', 'c'])
133
+
134
+ # ignore nested keys
135
+ assert_that({'a': {'b': 2, 'c': 3, 'd': 4}}).is_equal_to(
136
+ {'a': {'d': 4}}, ignore=[('a', 'b'), ('a', 'c')]
137
+ )
138
+
139
+ When the val is *dict-like*, only certain keys can be *included* when checking equality:
140
+
141
+ # include a single key
142
+ assert_that({'a': 1, 'b': 2}).is_equal_to({'a': 1}, include='a')
143
+
144
+ # include multiple keys
145
+ assert_that({'a': 1, 'b': 2, 'c': 3}).is_equal_to({'a': 1, 'b': 2}, include=['a', 'b'])
146
+
147
+ Works with dataclasses, namedtuples, attrs, and Pydantic models:
148
+
149
+ @dataclass
150
+ class User:
151
+ id: int
152
+ name: str
153
+
154
+ assert_that(User(id=1, name="Alice")).is_equal_to(User(id=99, name="Alice"), ignore="id")
155
+
156
+ Compares lists of objects pairwise:
157
+
158
+ actual = [User(id=1, name="Alice"), User(id=2, name="Bob")]
159
+ expected = [User(id=99, name="Alice"), User(id=99, name="Bob")]
160
+ assert_that(actual).is_equal_to(expected, ignore="id")
161
+
162
+ Compare nested floats with an absolute tolerance, or supply custom comparators:
163
+
164
+ assert_that({"price": 1.0001}).is_equal_to({"price": 1.0}, tolerance=0.001)
165
+
166
+ # by type, or by field name (field name wins over type)
167
+ assert_that(actual).is_equal_to(expected, comparators={float: lambda a, e: round(a, 2) == round(e, 2)})
168
+ assert_that(actual).is_equal_to(expected, comparators={"name": lambda a, e: a.lower() == e.lower()})
169
+
170
+ Ignore fields by regex or by type:
171
+
172
+ import re
173
+
174
+ assert_that(payload).is_equal_to(expected, ignore=re.compile(r"^_")) # ignore private-ish keys
175
+ assert_that(payload).is_equal_to(expected, ignore=float) # ignore all float fields
176
+
177
+ Require the same type at every level, which plain ``==`` does not:
178
+
179
+ assert_that({"active": True}).is_equal_to({"active": 1}) # passes
180
+ assert_that({"active": True}).is_equal_to({"active": 1}, strict_types=True) # fails
181
+
182
+ Failure produces a nice error message:
183
+
184
+ assert_that(1).is_equal_to(2) # fails
185
+ # Expected <1> to be equal to <2>, but was not.
186
+
187
+ Returns:
188
+ AssertionBuilder: returns this instance to chain to the next assertion
189
+
190
+ Raises:
191
+ AssertionError: if actual is **not** equal to expected
192
+ TypeError: if ``ignore``/``include`` is a one-shot or otherwise unsupported iterable, or is
193
+ used on a value that is neither dict-like nor has introspectable fields; if ``tolerance`` is
194
+ not a real number or ``comparators`` is not a dict of callables; or if val or other is (or
195
+ contains, at any nesting depth) an element-wise array/frame-like (numpy/pandas/polars) whose
196
+ ``==`` has no single truth value (compare the value's own equality, e.g.
197
+ ``actual.equals(expected)``, instead)
198
+ ValueError: if ``tolerance`` is ``NaN`` or negative
199
+
200
+ Tip:
201
+ Using [`is_equal_to()`][assertpy.base.BaseMixin.is_equal_to] with a ``float`` val is just
202
+ asking for trouble. Instead, you'll
203
+ always want to use *fuzzy* numeric assertions
204
+ like [`is_close_to()`][assertpy.numeric.NumericMixin.is_close_to]
205
+ or [`is_between()`][assertpy.numeric.NumericMixin.is_between].
206
+
207
+ See Also:
208
+ [`is_equal_to_ignoring_case()`][assertpy.string.StringMixin.is_equal_to_ignoring_case] -
209
+ for case-insensitive string equality
210
+ """
211
+ if not kwargs:
212
+ if type(self.val) in _EQ_ATOMIC and type(other) in _EQ_ATOMIC:
213
+ # atomic scalars: no array or dict likeness, and `==` yields a real bool
214
+ try:
215
+ if self.val == other:
216
+ return self
217
+ except decimal.InvalidOperation:
218
+ pass # only a signalling `Decimal` NaN signals between two atomic scalars, and it equals nothing
219
+ if isinstance(self.val, str) and isinstance(other, str):
220
+ actual_repr = _truncated(_elided_text_repr(self.val, other))
221
+ expected_repr = _truncated(_elided_text_repr(other, self.val))
222
+ else:
223
+ actual_repr, expected_repr = _disambiguated(self.val, other)
224
+ diff = _build_equality_diff(self.val, other)
225
+ self._compared = (self.val, other)
226
+ return self.error(
227
+ f"Expected <{actual_repr}> to be equal to <{expected_repr}>, but was not.",
228
+ actual=self.val,
229
+ expected=other,
230
+ diff=diff,
231
+ )
232
+ ignore = include = config = None
233
+ else:
234
+ reject_unknown_kwargs(kwargs, _IS_EQUAL_TO_OPTIONS, "is_equal_to")
235
+ ignore = kwargs.get("ignore")
236
+ include = kwargs.get("include")
237
+ config = _build_compare_config(
238
+ kwargs.get("tolerance"),
239
+ kwargs.get("comparators"),
240
+ kwargs.get("ignore_null", False),
241
+ kwargs.get("strict_types", False),
242
+ kwargs.get("rel"),
243
+ )
244
+
245
+ operand = _ambiguous_array_operand(self.val, other)
246
+ if operand is not None:
247
+ raise _array_equality_error("is_equal_to", operand)
248
+
249
+ # cleared however this leaves, since a soft block goes on using the builder after a failure it collected
250
+ # stored only for a call given comparators: a second attribute stored on every one cost a passing dict 200 ns
251
+ compared_through = config is not None and bool(config.comparators)
252
+ enclosing = False
253
+ try:
254
+ if compared_through:
255
+ enclosing, self._comparators_took_part = self._comparators_took_part, True
256
+ compared = self._compare_to(other, ignore=ignore, include=include, config=config)
257
+ finally:
258
+ self._equality_comparison = False
259
+ if compared_through:
260
+ # as it was: a comparator may assert on this same builder while the comparison it serves runs
261
+ self._comparators_took_part = enclosing
262
+ if self._compared_nothing:
263
+ self._compared_nothing = False
264
+ _warn_nothing_compared()
265
+ return compared
266
+
267
+ def _compare_to(self, other: object, *, ignore: object, include: object, config: _CompareConfig | None) -> Self:
268
+ """The dispatch of `is_equal_to` once its options are parsed, so the caller can scope them."""
269
+ # read once: code of the value can put another on the builder while it is compared and printed
270
+ val = self.val
271
+ if config is not None and config.comparators and _resolve_comparator(val, config, field=None) is not None:
272
+ # a comparator owns the root as it owns any node, ahead of strict types and of the key walk
273
+ return self._config_verdict(other, config)
274
+ if config is not None and config.strict_types and _types_differ(val, other):
275
+ # the key walk never sees the pair, so an OrderedDict against a dict would pass a strict comparison
276
+ actual_repr, expected_repr = _disambiguated(val, other)
277
+ return self.error(
278
+ f"Expected <{actual_repr}> to be equal to <{expected_repr}>, but was not.{_config_note(config)}",
279
+ actual=val,
280
+ expected=other,
281
+ diff=_build_equality_diff(val, other, config=config),
282
+ )
283
+
284
+ if mapping_shaped(val, check_values=False) and mapping_shaped(other, check_values=False):
285
+ if self._dict_not_equal(val, other, ignore=ignore, include=include, config=config):
286
+ self._dict_err(val, other, ignore=ignore, include=include, config=config)
287
+ else:
288
+ self._note_if_nothing_compared(val, other, ignore=ignore, include=include)
289
+ elif key_specs_given(ignore) or key_specs_given(include):
290
+ val_is_namedtuple = is_namedtuple(val)
291
+ other_is_namedtuple = is_namedtuple(other)
292
+ if (
293
+ isinstance(val, (list, tuple))
294
+ and isinstance(other, (list, tuple))
295
+ and not val_is_namedtuple
296
+ and not other_is_namedtuple
297
+ ):
298
+ self._seq_equal_with_filter(val, other, ignore=ignore, include=include, config=config)
299
+ else:
300
+ self._obj_equal_with_filter(val, other, ignore=ignore, include=include, config=config)
301
+ elif config is not None:
302
+ return self._config_verdict(other, config)
303
+ else:
304
+ # the one branch deciding with `==`, asked first since a type may rewrite its own `__eq__` while answering
305
+ self._equality_comparison = identity_candidate(val, other)
306
+ if not _guarded_equal(val, other):
307
+ if _both_list_like(val, other):
308
+ actual_repr = _truncated(_elided_seq_repr(val, other))
309
+ expected_repr = _truncated(_elided_seq_repr(other, val))
310
+ else:
311
+ actual_repr, expected_repr = _disambiguated(val, other)
312
+ diff = _build_equality_diff(val, other)
313
+ self._compared = (val, other)
314
+ return self.error(
315
+ f"Expected <{actual_repr}> to be equal to <{expected_repr}>, but was not.",
316
+ actual=val,
317
+ expected=other,
318
+ diff=diff,
319
+ )
320
+ return self
321
+
322
+ def _config_verdict(self, other: object, config: _CompareConfig) -> Self:
323
+ """`is_equal_to` under a compare config, decided by the structural walk from the root down."""
324
+ diff = _build_equality_diff(self.val, other, config=config)
325
+ if diff.entries:
326
+ actual_text, expected_text = _told_apart(
327
+ _truncated(_safe_str(self.val)), _truncated(_safe_str(other)), self.val, other
328
+ )
329
+ return self.error(
330
+ f"Expected <{actual_text}> to be equal to <{expected_text}>, but was not.{_config_note(config)}",
331
+ actual=self.val,
332
+ expected=other,
333
+ diff=diff,
334
+ )
335
+ return self
336
+
337
+ def _obj_equal_with_filter(self, actual, expected, *, ignore=None, include=None, config=None):
338
+ """Compare two objects by converting to dicts and applying ignore/include filters."""
339
+ # a plain dict against an object is what the sequence path already compares element by element
340
+ actual_dict, expected_dict = fields_pair(actual, expected)
341
+ if actual_dict is None or expected_dict is None:
342
+ raise TypeError(
343
+ "ignore/include requires dict-like objects or objects with introspectable fields"
344
+ " (dataclass, namedtuple, attrs, Pydantic model, or object with __dict__)"
345
+ )
346
+ if self._dict_not_equal(actual_dict, expected_dict, ignore=ignore, include=include, config=config):
347
+ # a dict read beside a record is a copy, and the failure hands back the caller's own
348
+ held = (
349
+ actual if isinstance(actual, dict) else actual_dict,
350
+ expected if isinstance(expected, dict) else expected_dict,
351
+ )
352
+ self._dict_err(actual_dict, expected_dict, ignore=ignore, include=include, config=config, held=held)
353
+ else:
354
+ self._note_if_nothing_compared(actual_dict, expected_dict, ignore=ignore, include=include)
355
+
356
+ def _note_if_nothing_compared(self, actual: object, expected: object, *, ignore: object, include: object) -> None:
357
+ """Keep, for `is_equal_to` to warn about, a pass whose key filter left nothing to compare."""
358
+ if (
359
+ (ignore is not None or include is not None)
360
+ and _guarding(self)
361
+ and filtered_to_nothing(actual, expected, ignore=ignore, include=include)
362
+ ):
363
+ self._compared_nothing = True
364
+
365
+ def _seq_equal_with_filter(self, actual, expected, *, ignore=None, include=None, config=None):
366
+ """Compare two sequences pairwise, an element with fields through them under ``ignore``/``include``.
367
+
368
+ The first element that differs, or a length that does, ends the comparison, and the failure is one for the
369
+ two sequences, as a dict's is. It used to be the element's own, which said neither where it stood nor what
370
+ else differed, and a length or a plain element failed with no diff at all. A list is no tuple under a
371
+ key option either, where the ``==`` of both declines the other (`_kinds_never_equal`).
372
+ """
373
+ if len(actual) == len(expected) and not _kinds_never_equal(actual, expected):
374
+ for actual_item, expected_item in zip(actual, expected, strict=True):
375
+ differs = self._item_differs(actual_item, expected_item, ignore, include, config)
376
+ if differs is None:
377
+ return
378
+ if differs:
379
+ break
380
+ else:
381
+ return
382
+ self._dict_err(actual, expected, ignore=ignore, include=include, config=config)
383
+
384
+ def _item_differs(self, actual_item, expected_item, ignore, include, config) -> bool | None:
385
+ """Whether one element of a sequence differs from its counterpart, or ``None`` once a prerequisite failed."""
386
+ actual_fields, expected_fields = fields_pair(actual_item, expected_item)
387
+ if actual_fields is not None and expected_fields is not None:
388
+ return self._dict_not_equal(actual_fields, expected_fields, ignore=ignore, include=include, config=config)
389
+ decision = _node_decision(actual_item, expected_item, config)
390
+ if decision in ("strict", "unanswered") or (decision == "recurse" and config is not None):
391
+ # the walk decides where `==` alone cannot: under strict types, on a graph, or under a config
392
+ return bool(_child_entries(actual_item, expected_item, _ROOT, descended_for=decision, config=config))
393
+ return decision != "equal"
394
+
395
+ def is_not_equal_to(self, other: object) -> Self:
396
+ """Asserts that val is not equal to other.
397
+
398
+ Checks actual is not equal to expected using the ``==`` operator, the question `is_equal_to`
399
+ asks. ``!=`` is never asked: a type's ``__ne__`` need not be the negation of its ``__eq__``.
400
+
401
+ Args:
402
+ other: the expected value
403
+
404
+ Examples:
405
+ Usage:
406
+
407
+ assert_that(1 + 2).is_not_equal_to(4)
408
+ assert_that('foo').is_not_equal_to('bar')
409
+ assert_that(123).is_not_equal_to(456)
410
+ assert_that(123.4).is_not_equal_to(567.8)
411
+ assert_that(['a', 'b']).is_not_equal_to(['c', 'd'])
412
+ assert_that((1, 2, 3)).is_not_equal_to((1, 2, 4))
413
+ assert_that({'a': 1, 'b': 2}).is_not_equal_to({'a': 1, 'b': 3})
414
+ assert_that({'a', 'b'}).is_not_equal_to({'a', 'x'})
415
+
416
+ Returns:
417
+ AssertionBuilder: returns this instance to chain to the next assertion
418
+
419
+ Raises:
420
+ AssertionError: if actual **is** equal to expected
421
+ TypeError: if val or other is (or contains, at any nesting depth) an element-wise
422
+ array/frame-like (numpy/pandas/polars) whose ``==`` has no single truth value; compare
423
+ the value's own equality instead
424
+ """
425
+ operand = _ambiguous_array_operand(self.val, other)
426
+ if operand is not None:
427
+ raise _array_equality_error("is_not_equal_to", operand)
428
+
429
+ if _guarded_equal(self.val, other, method="is_not_equal_to"):
430
+ return self.error(
431
+ f"Expected <{_truncated(_safe_str(self.val))}> to be not equal to"
432
+ f" <{_truncated(_safe_str(other))}>, but was."
433
+ )
434
+ return self
435
+
436
+ def is_same_as(self, other: object) -> Self:
437
+ """Asserts that val is identical to other.
438
+
439
+ Checks actual is identical to expected using the ``is`` operator.
440
+
441
+ Args:
442
+ other: the expected value
443
+
444
+ Examples:
445
+ Basic types are identical:
446
+
447
+ assert_that(1).is_same_as(1)
448
+ assert_that('foo').is_same_as('foo')
449
+ assert_that(123.4).is_same_as(123.4)
450
+
451
+ As are immutables like ``tuple``:
452
+
453
+ assert_that((1, 2, 3)).is_same_as((1, 2, 3))
454
+
455
+ But mutable collections like ``list``, ``dict``, and ``set`` are not:
456
+
457
+ # these all fail...
458
+ assert_that(['a', 'b']).is_same_as(['a', 'b']) # fails
459
+ assert_that({'a': 1, 'b': 2}).is_same_as({'a': 1, 'b': 2}) # fails
460
+ assert_that({'a', 'b'}).is_same_as({'a', 'b'}) # fails
461
+
462
+ Unless they are the same object:
463
+
464
+ x = {'a': 1, 'b': 2}
465
+ y = x
466
+ assert_that(x).is_same_as(y)
467
+
468
+ Returns:
469
+ AssertionBuilder: returns this instance to chain to the next assertion
470
+
471
+ Raises:
472
+ AssertionError: if actual is **not** identical to expected
473
+ """
474
+ if self.val is not other:
475
+ return self.error(
476
+ f"Expected <{_capped(self.val)}> to be identical to <{_capped_format(other)}>, but was not.",
477
+ expected=other,
478
+ )
479
+ return self
480
+
481
+ def is_not_same_as(self, other: object) -> Self:
482
+ """Asserts that val is not identical to other.
483
+
484
+ Checks actual is not identical to expected using the ``is`` operator.
485
+
486
+ Args:
487
+ other: the expected value
488
+
489
+ Examples:
490
+ Usage:
491
+
492
+ assert_that(1).is_not_same_as(2)
493
+ assert_that('foo').is_not_same_as('bar')
494
+ assert_that(123.4).is_not_same_as(567.8)
495
+ assert_that((1, 2, 3)).is_not_same_as((1, 2, 4))
496
+
497
+ # mutable collections, even if equal, are not identical...
498
+ assert_that(['a', 'b']).is_not_same_as(['a', 'b'])
499
+ assert_that({'a': 1, 'b': 2}).is_not_same_as({'a': 1, 'b': 2})
500
+ assert_that({'a', 'b'}).is_not_same_as({'a', 'b'})
501
+
502
+ Returns:
503
+ AssertionBuilder: returns this instance to chain to the next assertion
504
+
505
+ Raises:
506
+ AssertionError: if actual **is** identical to expected
507
+ """
508
+ if self.val is other:
509
+ return self.error(
510
+ f"Expected <{_capped(self.val)}> to be not identical to <{_capped_format(other)}>, but was."
511
+ )
512
+ return self
513
+
514
+ def is_true(self) -> Self:
515
+ """Asserts that val is true.
516
+
517
+ Examples:
518
+ Usage:
519
+
520
+ assert_that(True).is_true()
521
+ assert_that(1).is_true()
522
+ assert_that('foo').is_true()
523
+ assert_that(1.0).is_true()
524
+ assert_that(['a', 'b']).is_true()
525
+ assert_that((1, 2, 3)).is_true()
526
+ assert_that({'a': 1, 'b': 2}).is_true()
527
+ assert_that({'a', 'b'}).is_true()
528
+
529
+ Returns:
530
+ AssertionBuilder: returns this instance to chain to the next assertion
531
+
532
+ Raises:
533
+ AssertionError: if val **is** false
534
+ TypeError: if val is a coroutine, since every coroutine is truthy and this would hold
535
+ whatever awaiting it would have answered
536
+ """
537
+ if not verdict(self.val, subject="the call under test"):
538
+ return self.error(f"Expected <{_capped(self.val)}> to be <True>, but was not.", expected=True)
539
+ return self
540
+
541
+ def is_false(self) -> Self:
542
+ """Asserts that val is false.
543
+
544
+ Examples:
545
+ Usage:
546
+
547
+ assert_that(False).is_false()
548
+ assert_that(0).is_false()
549
+ assert_that('').is_false()
550
+ assert_that(0.0).is_false()
551
+ assert_that([]).is_false()
552
+ assert_that(()).is_false()
553
+ assert_that({}).is_false()
554
+ assert_that(set()).is_false()
555
+
556
+ Returns:
557
+ AssertionBuilder: returns this instance to chain to the next assertion
558
+
559
+ Raises:
560
+ AssertionError: if val **is** true
561
+ TypeError: if val is a coroutine, since every coroutine is truthy and this would refuse
562
+ whatever awaiting it would have answered
563
+ """
564
+ if verdict(self.val, subject="the call under test"):
565
+ return self.error(f"Expected <{_capped(self.val)}> to be <False>, but was not.", expected=False)
566
+ return self
567
+
568
+ def is_none(self) -> Self:
569
+ """Asserts that val is none.
570
+
571
+ Examples:
572
+ Usage:
573
+
574
+ assert_that(None).is_none()
575
+ assert_that(print('hello world')).is_none()
576
+
577
+ Returns:
578
+ AssertionBuilder: returns this instance to chain to the next assertion
579
+
580
+ Raises:
581
+ AssertionError: if val is **not** none
582
+ """
583
+ if self.val is not None:
584
+ return self.error(f"Expected <{_capped(self.val)}> to be <None>, but was not.", expected=None)
585
+ return self
586
+
587
+ def is_not_none(self) -> Self:
588
+ """Asserts that val is not none.
589
+
590
+ Examples:
591
+ Usage:
592
+
593
+ assert_that(0).is_not_none()
594
+ assert_that('foo').is_not_none()
595
+ assert_that(False).is_not_none()
596
+
597
+ Returns:
598
+ AssertionBuilder: returns this instance to chain to the next assertion
599
+
600
+ Raises:
601
+ AssertionError: if val **is** none
602
+ """
603
+ if self.val is None:
604
+ return self.error("Expected not <None>, but was.")
605
+ return self
606
+
607
+ @staticmethod
608
+ def _type(val):
609
+ if hasattr(val, "__name__"):
610
+ return val.__name__
611
+ return val.__class__.__name__
612
+
613
+ def is_type_of(self, some_type: type) -> Self:
614
+ """Asserts that val is of the given type.
615
+
616
+ Args:
617
+ some_type (type): the expected type
618
+
619
+ Examples:
620
+ Usage:
621
+
622
+ assert_that(1).is_type_of(int)
623
+ assert_that('foo').is_type_of(str)
624
+ assert_that(123.4).is_type_of(float)
625
+ assert_that(['a', 'b']).is_type_of(list)
626
+ assert_that((1, 2, 3)).is_type_of(tuple)
627
+ assert_that({'a': 1, 'b': 2}).is_type_of(dict)
628
+ assert_that({'a', 'b'}).is_type_of(set)
629
+ assert_that(True).is_type_of(bool)
630
+
631
+ Returns:
632
+ AssertionBuilder: returns this instance to chain to the next assertion
633
+
634
+ Raises:
635
+ AssertionError: if val is **not** of the given type
636
+ """
637
+ if type(some_type) is not type and not issubclass(type(some_type), type):
638
+ refuse(some_type, "a type", subject=argument("type"))
639
+ if type(self.val) is not some_type:
640
+ type_name = self._type(self.val)
641
+ return self.error(
642
+ f"Expected <{_capped(self.val)}:{type_name}> to be of type <{some_type.__name__}>, but was not.",
643
+ expected=some_type,
644
+ )
645
+ return self
646
+
647
+ def is_instance_of(self, some_class: ClassInfo) -> Self:
648
+ """Asserts that val is an instance of the given class.
649
+
650
+ Args:
651
+ some_class: the expected class, a union of them, or a tuple nested to any depth
652
+
653
+ Examples:
654
+ Usage:
655
+
656
+ assert_that(1).is_instance_of(int)
657
+ assert_that('foo').is_instance_of(str)
658
+ assert_that(123.4).is_instance_of(float)
659
+ assert_that(['a', 'b']).is_instance_of(list)
660
+ assert_that((1, 2, 3)).is_instance_of(tuple)
661
+ assert_that({'a': 1, 'b': 2}).is_instance_of(dict)
662
+ assert_that({'a', 'b'}).is_instance_of(set)
663
+ assert_that(True).is_instance_of(bool)
664
+
665
+ With a user-defined class:
666
+
667
+ class Foo: pass
668
+ f = Foo()
669
+ assert_that(f).is_instance_of(Foo)
670
+ assert_that(f).is_instance_of(object)
671
+
672
+ With alternatives, anything `isinstance` takes, nested to any depth:
673
+
674
+ assert_that(1).is_instance_of(int | str)
675
+ assert_that(1).is_instance_of((int, str))
676
+ assert_that(1).is_instance_of((int, (str, bytes)))
677
+
678
+ Returns:
679
+ AssertionBuilder: returns this instance to chain to the next assertion
680
+
681
+ Raises:
682
+ AssertionError: if val is **not** an instance of the given class
683
+ TypeError: if the given arg, or any member of a union or tuple, is not a class
684
+ """
685
+ # a class answers `isinstance` itself, raising what it raises, though a generic alias passes for one on 3.10
686
+ if not isinstance(some_class, type) or isinstance(some_class, GenericAlias):
687
+ _require_class_info(some_class, name="class", probe=self.val)
688
+ if not isinstance(self.val, some_class):
689
+ type_name = self._type(self.val)
690
+ some_class_name = _type_expression_name(some_class)
691
+ return self.error(
692
+ f"Expected <{_capped(self.val)}:{type_name}> to be instance of class <{some_class_name}>, but was not.",
693
+ expected=some_class,
694
+ )
695
+ return self
696
+
697
+ def is_instance_of_any(self, *some_classes: ClassInfo) -> Self:
698
+ """Asserts that val is an instance of at least one of the given classes.
699
+
700
+ Args:
701
+ *some_classes: the candidate classes, each a class, a union of them, or a nested tuple
702
+
703
+ Examples:
704
+ Usage:
705
+
706
+ assert_that(1).is_instance_of_any(int, float)
707
+ assert_that('foo').is_instance_of_any(str, bytes)
708
+ assert_that(TimeoutError()).is_instance_of_any(OSError, ValueError)
709
+ assert_that(1).is_instance_of_any(int | str, (bytes, bytearray))
710
+
711
+ Returns:
712
+ AssertionBuilder: returns this instance to chain to the next assertion
713
+
714
+ Raises:
715
+ AssertionError: if val is **not** an instance of any of the given classes
716
+ TypeError: if a given arg is not a class
717
+ ValueError: if no classes are given
718
+ """
719
+ if len(some_classes) == 0:
720
+ raise ValueError("one or more args must be given")
721
+ _require_class_info(some_classes, name="class", expectation="classes", probe=self.val)
722
+ if not isinstance(self.val, some_classes):
723
+ type_name = self._type(self.val)
724
+ class_names = ", ".join(_type_expression_name(some_class) for some_class in some_classes)
725
+ return self.error(
726
+ f"Expected <{_capped(self.val)}:{type_name}> to be instance of any of <{class_names}>, but was not.",
727
+ expected=some_classes,
728
+ )
729
+ return self
730
+
731
+ def is_subclass_of(self, some_class: ClassInfo) -> Self:
732
+ """Asserts that val is a class and is a subclass of the given class.
733
+
734
+ Checks the class hierarchy using the ``issubclass()`` built-in, so a class counts as a
735
+ subclass of itself.
736
+
737
+ Args:
738
+ some_class: the expected ancestor class, a union of them, or a tuple nested to any depth
739
+
740
+ Examples:
741
+ Usage:
742
+
743
+ assert_that(bool).is_subclass_of(int)
744
+ assert_that(TimeoutError).is_subclass_of(OSError)
745
+
746
+ class Base: pass
747
+ class Derived(Base): pass
748
+
749
+ assert_that(Derived).is_subclass_of(Base)
750
+ assert_that(Derived).is_subclass_of(Derived)
751
+
752
+ Returns:
753
+ AssertionBuilder: returns this instance to chain to the next assertion
754
+
755
+ Raises:
756
+ AssertionError: if val is **not** a subclass of the given class
757
+ TypeError: if val or the given arg, or any member of a union or tuple, is not a class
758
+ """
759
+ require_type(self.val, type, "a class")
760
+ # as in `is_instance_of`, with `issubclass` asking each member
761
+ if not isinstance(some_class, type) or isinstance(some_class, GenericAlias):
762
+ _require_class_info(some_class, name="class", probe=self.val, check=issubclass)
763
+ if not issubclass(self.val, some_class):
764
+ expected_name = _type_expression_name(some_class)
765
+ return self.error(
766
+ f"Expected <{self.val.__name__}> to be subclass of <{expected_name}>, but was not.",
767
+ expected=some_class,
768
+ )
769
+ return self
770
+
771
+ def is_length(self, length: SupportsIndex | Matcher[int]) -> Self:
772
+ """Asserts that val is the given length.
773
+
774
+ Checks val is the given length using the ``len()`` built-in. A matcher in place of the number is
775
+ asked about the length, so a bound or a range reads as one assertion: ``is_length(match.between(2,
776
+ 5))``.
777
+
778
+ Args:
779
+ length (int | Matcher): the expected length, or a matcher the length has to satisfy
780
+
781
+ Examples:
782
+ Usage:
783
+
784
+ assert_that('foo').is_length(3)
785
+ assert_that(['a', 'b']).is_length(2)
786
+ assert_that((1, 2, 3)).is_length(3)
787
+ assert_that({'a': 1, 'b': 2}).is_length(2)
788
+ assert_that({'a', 'b'}).is_length(2)
789
+ assert_that(['a', 'b']).is_length(match.greater_than(1))
790
+
791
+ Returns:
792
+ AssertionBuilder: returns this instance to chain to the next assertion
793
+
794
+ Raises:
795
+ AssertionError: if val is **not** the given length
796
+ """
797
+ if _is_matcher(length):
798
+ return self._length_satisfies(length)
799
+ wanted = length if type(length) is int else require_integer(length, "length")
800
+ if wanted < 0:
801
+ raise ValueError("given arg must be a positive int")
802
+ if sized_len(self.val) != wanted:
803
+ return self.error(
804
+ f"Expected <{_capped(self.val)}> to be of length <{length}>, but was <{sized_len(self.val)}>.",
805
+ expected=length,
806
+ )
807
+ return self
808
+
809
+ def _length_satisfies(self, matcher: Matcher[int]) -> Self:
810
+ """`is_length()` with a matcher for the length, which is asked about the number `len()` gives."""
811
+ size = sized_len(self.val)
812
+ outcome = _outcome_of(matcher, _ReadOnce(size))
813
+ if outcome is not None:
814
+ # the reason a leaf gives about a bare number is that number again
815
+ reason = "" if outcome.mismatch == f"was <{size}>" else f": {outcome.mismatch}"
816
+ return self.error(
817
+ f"Expected <{_capped(self.val)}> to have a length that is {outcome.description},"
818
+ f" but was <{size}>{reason}.",
819
+ expected=matcher,
820
+ )
821
+ return self
822
+
823
+ def is_length_between(self, low: SupportsIndex, high: SupportsIndex) -> Self:
824
+ """Asserts that val's length is between low and high (both inclusive).
825
+
826
+ Checks val's length using the ``len()`` built-in, like
827
+ [`is_length()`][assertpy.base.BaseMixin.is_length]. Identical to
828
+ [`has_size_between()`][assertpy.collection.CollectionMixin.has_size_between] apart from
829
+ the error message wording.
830
+
831
+ Args:
832
+ low: the inclusive lower length bound
833
+ high: the inclusive upper length bound
834
+
835
+ Examples:
836
+ Usage:
837
+
838
+ assert_that('foo').is_length_between(1, 5)
839
+ assert_that(['a', 'b']).is_length_between(2, 2)
840
+ assert_that((1, 2, 3)).is_length_between(0, 3)
841
+
842
+ Returns:
843
+ AssertionBuilder: returns this instance to chain to the next assertion
844
+
845
+ Raises:
846
+ AssertionError: if val's length is **not** between low and high
847
+ TypeError: if a given arg is not an int
848
+ ValueError: if a given arg is negative, or low is greater than high
849
+ """
850
+ least = low if type(low) is int else require_integer(low, "low")
851
+ most = high if type(high) is int else require_integer(high, "high")
852
+ if least < 0 or most < 0:
853
+ raise ValueError("given args must be positive ints")
854
+ if least > most:
855
+ raise ValueError("given low arg must be less than given high arg")
856
+ if not least <= sized_len(self.val) <= most:
857
+ return self.error(
858
+ f"Expected <{_capped(self.val)}> to be of length between <{low}> and <{high}>, "
859
+ f"but was <{sized_len(self.val)}>.",
860
+ expected=(low, high),
861
+ )
862
+ return self