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