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/string.py
ADDED
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import collections.abc
|
|
4
|
+
import decimal
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from ._engine._mixin_base import _MixinBase
|
|
8
|
+
from ._engine._ordering import broadcasts, equal_past
|
|
9
|
+
from ._engine._require import argument, refuse, require_pattern, require_type, sized_len
|
|
10
|
+
from ._hints import but_for_case, but_for_whitespace, not_at_an_end, not_in_text, under
|
|
11
|
+
from .errors import _capped, _capped_format, _capped_repr, _first_difference, _formatted, _parted, _safe_str
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
import re
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
|
|
17
|
+
from ._engine._compat import Self
|
|
18
|
+
|
|
19
|
+
__tracebackhide__ = True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parted_from_the_end(text: str, ending: str) -> tuple[int, int]:
|
|
23
|
+
"""Where *text* stops matching *ending*, read from their ends: the place in each."""
|
|
24
|
+
shared = _first_difference(text[::-1], ending[::-1])
|
|
25
|
+
return len(text) - shared - 1, len(ending) - shared - 1
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _raw_place(text: str, lowered: str, place: int) -> int:
|
|
29
|
+
"""The place in *text* of the character that *place* in its lowered form came from.
|
|
30
|
+
|
|
31
|
+
Lowering can lengthen a text, ``İ`` becoming two characters, so a place found in the lowered form runs
|
|
32
|
+
ahead of the raw one by one for each such character before it: by 3000 after 3000 of them, which put the
|
|
33
|
+
cut past the difference it was made for.
|
|
34
|
+
"""
|
|
35
|
+
if len(lowered) == len(text):
|
|
36
|
+
return place
|
|
37
|
+
reached = 0
|
|
38
|
+
for index, character in enumerate(text):
|
|
39
|
+
reached += len(character.lower())
|
|
40
|
+
if reached > place:
|
|
41
|
+
return index
|
|
42
|
+
return len(text)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parted_past_spacing(text: str, other: str) -> tuple[int, int]:
|
|
46
|
+
"""Where two texts first differ with whitespace left out, as the place in each."""
|
|
47
|
+
at = other_at = 0
|
|
48
|
+
while True:
|
|
49
|
+
while at < len(text) and text[at].isspace():
|
|
50
|
+
at += 1
|
|
51
|
+
while other_at < len(other) and other[other_at].isspace():
|
|
52
|
+
other_at += 1
|
|
53
|
+
if at >= len(text) or other_at >= len(other) or text[at] != other[other_at]:
|
|
54
|
+
return at, other_at
|
|
55
|
+
at += 1
|
|
56
|
+
other_at += 1
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class StringMixin(_MixinBase):
|
|
60
|
+
"""String assertions mixin."""
|
|
61
|
+
|
|
62
|
+
def _assert_string_chars(self, is_valid: Callable[[str], bool], description: str) -> Self:
|
|
63
|
+
"""Shared shape for the character-class string assertions.
|
|
64
|
+
|
|
65
|
+
Validates that val is a non-empty string, then emits a "contain only ``description``" error if
|
|
66
|
+
``is_valid(self.val)`` is falsy.
|
|
67
|
+
"""
|
|
68
|
+
require_type(self.val, str, "a string")
|
|
69
|
+
if sized_len(self.val) == 0:
|
|
70
|
+
raise ValueError("val is empty")
|
|
71
|
+
if not is_valid(self.val):
|
|
72
|
+
return self.error(
|
|
73
|
+
f"Expected <{_capped(self.val)}> to contain only {description}, but did not.", expected=description
|
|
74
|
+
)
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
def is_equal_to_ignoring_case(self, other: str) -> Self:
|
|
78
|
+
"""Asserts that val is a string and is case-insensitive equal to other.
|
|
79
|
+
|
|
80
|
+
Checks actual is equal to expected using the ``==`` operator and ``str.lower()``.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
other: the expected value
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
Usage:
|
|
87
|
+
|
|
88
|
+
assert_that('foo').is_equal_to_ignoring_case('FOO')
|
|
89
|
+
assert_that('FOO').is_equal_to_ignoring_case('foo')
|
|
90
|
+
assert_that('fOo').is_equal_to_ignoring_case('FoO')
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
AssertionError: if actual is **not** case-insensitive equal to expected
|
|
97
|
+
"""
|
|
98
|
+
require_type(self.val, str, "a string")
|
|
99
|
+
require_type(other, str, "a string", subject=argument("other"))
|
|
100
|
+
if self.val.lower() == other.lower():
|
|
101
|
+
return self
|
|
102
|
+
lowered, other_lowered = self.val.lower(), other.lower()
|
|
103
|
+
parted = _first_difference(lowered, other_lowered)
|
|
104
|
+
actual_text, other_text = _parted(
|
|
105
|
+
_safe_str(self.val),
|
|
106
|
+
_formatted(other),
|
|
107
|
+
_raw_place(self.val, lowered, parted),
|
|
108
|
+
_raw_place(other, other_lowered, parted),
|
|
109
|
+
)
|
|
110
|
+
return self.error(
|
|
111
|
+
f"Expected <{actual_text}> to be case-insensitive equal to <{other_text}>, but was not."
|
|
112
|
+
f"{under(but_for_whitespace(self.val, other))}",
|
|
113
|
+
expected=other,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def is_equal_to_ignoring_whitespace(self, other: str) -> Self:
|
|
117
|
+
"""Asserts that val is a string and is equal to other ignoring all whitespace.
|
|
118
|
+
|
|
119
|
+
All whitespace (spaces, tabs, newlines) is stripped from both strings before comparing with
|
|
120
|
+
the ``==`` operator, so differences in spacing, indentation, or line breaks don't matter.
|
|
121
|
+
Case still does.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
other: the expected value
|
|
125
|
+
|
|
126
|
+
Examples:
|
|
127
|
+
Usage:
|
|
128
|
+
|
|
129
|
+
assert_that('foo bar').is_equal_to_ignoring_whitespace('foobar')
|
|
130
|
+
assert_that('foo\\nbar').is_equal_to_ignoring_whitespace('foo bar')
|
|
131
|
+
assert_that(' foo ').is_equal_to_ignoring_whitespace('foo')
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
AssertionError: if actual is **not** equal to expected ignoring whitespace
|
|
138
|
+
TypeError: if val or the given arg is not a string
|
|
139
|
+
"""
|
|
140
|
+
require_type(self.val, str, "a string")
|
|
141
|
+
require_type(other, str, "a string", subject=argument("other"))
|
|
142
|
+
if "".join(self.val.split()) == "".join(other.split()):
|
|
143
|
+
return self
|
|
144
|
+
actual_text, other_text = _parted(
|
|
145
|
+
_safe_str(self.val), _formatted(other), *_parted_past_spacing(self.val, other)
|
|
146
|
+
)
|
|
147
|
+
return self.error(
|
|
148
|
+
f"Expected <{actual_text}> to be equal to <{other_text}> ignoring whitespace, but was not."
|
|
149
|
+
f"{under(but_for_case(self.val, other))}",
|
|
150
|
+
expected=other,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def contains_ignoring_case(self, *items: str) -> Self:
|
|
154
|
+
"""Asserts that val is string and contains the given item or items.
|
|
155
|
+
|
|
156
|
+
Walks val and checks for item or items using the ``==`` operator and ``str.lower()``.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
*items: the item or items expected to be contained
|
|
160
|
+
|
|
161
|
+
Examples:
|
|
162
|
+
Usage:
|
|
163
|
+
|
|
164
|
+
assert_that('foo').contains_ignoring_case('F', 'oO')
|
|
165
|
+
assert_that(['a', 'B']).contains_ignoring_case('A', 'b')
|
|
166
|
+
assert_that({'a': 1, 'B': 2}).contains_ignoring_case('A', 'b')
|
|
167
|
+
assert_that({'a', 'B'}).contains_ignoring_case('A', 'b')
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
AssertionError: if val does **not** contain the case-insensitive item or items
|
|
174
|
+
"""
|
|
175
|
+
if len(items) == 0:
|
|
176
|
+
raise ValueError("one or more args must be given")
|
|
177
|
+
if isinstance(self.val, str):
|
|
178
|
+
if len(items) == 1:
|
|
179
|
+
require_type(items[0], str, "a string", subject=argument("item"))
|
|
180
|
+
if items[0].lower() not in self.val.lower():
|
|
181
|
+
return self.error(
|
|
182
|
+
f"Expected <{_capped(self.val)}> to case-insensitive contain item"
|
|
183
|
+
f" <{_capped_format(items[0])}>, but did not."
|
|
184
|
+
f"{under(not_in_text(self.val, items[0], cased=False))}",
|
|
185
|
+
expected=items[0],
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
val_lower = self.val.lower()
|
|
189
|
+
missing = []
|
|
190
|
+
for item in items:
|
|
191
|
+
require_type(item, str, "a string", subject=argument("item"))
|
|
192
|
+
if item.lower() not in val_lower:
|
|
193
|
+
missing.append(item)
|
|
194
|
+
if missing:
|
|
195
|
+
return self.error(
|
|
196
|
+
f"Expected <{_capped(self.val)}> to case-insensitive contain items"
|
|
197
|
+
f" {self._fmt_items(items)}, but did not contain {self._fmt_items(missing)}.",
|
|
198
|
+
expected=items,
|
|
199
|
+
)
|
|
200
|
+
elif isinstance(self.val, collections.abc.Iterable):
|
|
201
|
+
lowered_values = []
|
|
202
|
+
for value in list(self._walked()): # read whole: the items are asked for their text, then compared
|
|
203
|
+
require_type(value, str, "a string", subject="every item of val")
|
|
204
|
+
lowered_values.append(value.lower())
|
|
205
|
+
missing = []
|
|
206
|
+
for item in items:
|
|
207
|
+
require_type(item, str, "a string", subject=argument("item"))
|
|
208
|
+
if item.lower() not in lowered_values:
|
|
209
|
+
missing.append(item)
|
|
210
|
+
if missing:
|
|
211
|
+
return self.error(
|
|
212
|
+
f"Expected <{_capped(self.val)}> to case-insensitive contain items"
|
|
213
|
+
f" {self._fmt_items(items)}, but did not contain {self._fmt_items(missing)}.",
|
|
214
|
+
expected=items,
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
refuse(self.val, "a string or an iterable")
|
|
218
|
+
return self
|
|
219
|
+
|
|
220
|
+
# `object` and not `str`: the body takes text, bytes or any element, and the three typed views each narrow it
|
|
221
|
+
def starts_with(self, prefix: object) -> Self:
|
|
222
|
+
"""Asserts that val is string or iterable and starts with prefix.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
prefix: the prefix
|
|
226
|
+
|
|
227
|
+
Examples:
|
|
228
|
+
Usage:
|
|
229
|
+
|
|
230
|
+
assert_that('foo').starts_with('fo')
|
|
231
|
+
assert_that(['a', 'b', 'c']).starts_with('a')
|
|
232
|
+
assert_that((1, 2, 3)).starts_with(1)
|
|
233
|
+
assert_that(((1, 2), (3, 4), (5, 6))).starts_with((1, 2))
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
AssertionError: if val does **not** start with prefix
|
|
240
|
+
"""
|
|
241
|
+
if prefix is None: # `None` is neither, and reaches the branch below only to confuse it
|
|
242
|
+
refuse(prefix, "a string or bytes", subject=argument("prefix"))
|
|
243
|
+
if isinstance(self.val, str):
|
|
244
|
+
text_prefix = require_type(prefix, str, "a string", subject=argument("prefix"))
|
|
245
|
+
if len(text_prefix) == 0:
|
|
246
|
+
raise ValueError("given prefix arg must not be empty")
|
|
247
|
+
if not self.val.startswith(text_prefix):
|
|
248
|
+
parted = _first_difference(self.val, text_prefix)
|
|
249
|
+
actual_text, prefix_text = _parted(_safe_str(self.val), _formatted(text_prefix), parted)
|
|
250
|
+
return self.error(
|
|
251
|
+
f"Expected <{actual_text}> to start with <{prefix_text}>, but did not."
|
|
252
|
+
f"{under(not_at_an_end(self.val, text_prefix, start=True))}",
|
|
253
|
+
expected=prefix,
|
|
254
|
+
)
|
|
255
|
+
elif isinstance(self.val, (bytes, bytearray)):
|
|
256
|
+
# bytes are iterable: `b"foo"` would yield 102, failing an assertion that should pass
|
|
257
|
+
raw_prefix = require_type(prefix, (bytes, bytearray), "bytes", subject=argument("prefix"))
|
|
258
|
+
if len(raw_prefix) == 0:
|
|
259
|
+
raise ValueError("given prefix arg must not be empty")
|
|
260
|
+
if not self.val.startswith(raw_prefix):
|
|
261
|
+
return self.error(
|
|
262
|
+
f"Expected <{_capped_repr(self.val)}> to start with <{_capped_repr(raw_prefix)}>, but did not.",
|
|
263
|
+
expected=prefix,
|
|
264
|
+
)
|
|
265
|
+
elif isinstance(self.val, collections.abc.Iterable):
|
|
266
|
+
iterator = iter(self._walked())
|
|
267
|
+
try:
|
|
268
|
+
first = next(iterator)
|
|
269
|
+
except StopIteration:
|
|
270
|
+
raise ValueError("val must not be empty") from None
|
|
271
|
+
try:
|
|
272
|
+
starts = first == prefix
|
|
273
|
+
starts = starts if type(starts) is bool or not broadcasts(first, prefix, answer=starts) else False
|
|
274
|
+
except (decimal.InvalidOperation, OverflowError, TypeError, ValueError) as refusal:
|
|
275
|
+
starts = equal_past(first, prefix, refusal)
|
|
276
|
+
if not starts:
|
|
277
|
+
return self.error(
|
|
278
|
+
f"Expected {_capped(self.val)} to start with <{_capped_format(prefix)}>, but did not.",
|
|
279
|
+
expected=prefix,
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
refuse(self.val, "a string or an iterable")
|
|
283
|
+
return self
|
|
284
|
+
|
|
285
|
+
# `object` and not `str`, for the same reason as `starts_with`
|
|
286
|
+
def ends_with(self, suffix: object) -> Self:
|
|
287
|
+
"""Asserts that val is string or iterable and ends with suffix.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
suffix: the suffix
|
|
291
|
+
|
|
292
|
+
Examples:
|
|
293
|
+
Usage:
|
|
294
|
+
|
|
295
|
+
assert_that('foo').ends_with('oo')
|
|
296
|
+
assert_that(['a', 'b', 'c']).ends_with('c')
|
|
297
|
+
assert_that((1, 2, 3)).ends_with(3)
|
|
298
|
+
assert_that(((1, 2), (3, 4), (5, 6))).ends_with((5, 6))
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
302
|
+
|
|
303
|
+
Raises:
|
|
304
|
+
AssertionError: if val does **not** end with suffix
|
|
305
|
+
"""
|
|
306
|
+
if suffix is None:
|
|
307
|
+
refuse(suffix, "a string or bytes", subject=argument("suffix"))
|
|
308
|
+
if isinstance(self.val, str):
|
|
309
|
+
text_suffix = require_type(suffix, str, "a string", subject=argument("suffix"))
|
|
310
|
+
if len(text_suffix) == 0:
|
|
311
|
+
raise ValueError("given suffix arg must not be empty")
|
|
312
|
+
if not self.val.endswith(text_suffix):
|
|
313
|
+
actual_text, suffix_text = _parted(
|
|
314
|
+
_safe_str(self.val), _formatted(text_suffix), *_parted_from_the_end(self.val, text_suffix)
|
|
315
|
+
)
|
|
316
|
+
return self.error(
|
|
317
|
+
f"Expected <{actual_text}> to end with <{suffix_text}>, but did not."
|
|
318
|
+
f"{under(not_at_an_end(self.val, text_suffix, start=False))}",
|
|
319
|
+
expected=suffix,
|
|
320
|
+
)
|
|
321
|
+
elif isinstance(self.val, (bytes, bytearray)):
|
|
322
|
+
# the mirror of the branch in `starts_with`: the last element of `b"foo"` is the int 111
|
|
323
|
+
raw_suffix = require_type(suffix, (bytes, bytearray), "bytes", subject=argument("suffix"))
|
|
324
|
+
if len(raw_suffix) == 0:
|
|
325
|
+
raise ValueError("given suffix arg must not be empty")
|
|
326
|
+
if not self.val.endswith(raw_suffix):
|
|
327
|
+
return self.error(
|
|
328
|
+
f"Expected <{_capped_repr(self.val)}> to end with <{_capped_repr(raw_suffix)}>, but did not.",
|
|
329
|
+
expected=suffix,
|
|
330
|
+
)
|
|
331
|
+
elif isinstance(self.val, collections.abc.Iterable):
|
|
332
|
+
items = list(self._walked())
|
|
333
|
+
if not items:
|
|
334
|
+
raise ValueError("val must not be empty")
|
|
335
|
+
try:
|
|
336
|
+
ends = items[-1] == suffix
|
|
337
|
+
ends = ends if type(ends) is bool or not broadcasts(items[-1], suffix, answer=ends) else False
|
|
338
|
+
except (decimal.InvalidOperation, OverflowError, TypeError, ValueError) as refusal:
|
|
339
|
+
ends = equal_past(items[-1], suffix, refusal)
|
|
340
|
+
if not ends:
|
|
341
|
+
return self.error(
|
|
342
|
+
f"Expected {_capped(self.val)} to end with <{_capped_format(suffix)}>, but did not.",
|
|
343
|
+
expected=suffix,
|
|
344
|
+
)
|
|
345
|
+
else:
|
|
346
|
+
refuse(self.val, "a string or an iterable")
|
|
347
|
+
return self
|
|
348
|
+
|
|
349
|
+
def starts_with_ignoring_case(self, prefix: str) -> Self:
|
|
350
|
+
"""Asserts that val is a string and starts with prefix, ignoring case.
|
|
351
|
+
|
|
352
|
+
Like [`starts_with()`][assertpy.string.StringMixin.starts_with] but case-insensitive
|
|
353
|
+
(via ``str.lower()``), and strings only.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
prefix: the prefix
|
|
357
|
+
|
|
358
|
+
Examples:
|
|
359
|
+
Usage:
|
|
360
|
+
|
|
361
|
+
assert_that('FooBar').starts_with_ignoring_case('foo')
|
|
362
|
+
assert_that('foobar').starts_with_ignoring_case('FOO')
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
366
|
+
|
|
367
|
+
Raises:
|
|
368
|
+
AssertionError: if val does **not** case-insensitive start with prefix
|
|
369
|
+
TypeError: if val or the given prefix is not a string
|
|
370
|
+
ValueError: if the given prefix is empty
|
|
371
|
+
"""
|
|
372
|
+
require_type(self.val, str, "a string")
|
|
373
|
+
require_type(prefix, str, "a string", subject=argument("prefix"))
|
|
374
|
+
if len(prefix) == 0:
|
|
375
|
+
raise ValueError("given prefix arg must not be empty")
|
|
376
|
+
if not self.val.lower().startswith(prefix.lower()):
|
|
377
|
+
lowered, prefix_lowered = self.val.lower(), prefix.lower()
|
|
378
|
+
parted = _first_difference(lowered, prefix_lowered)
|
|
379
|
+
actual_text, prefix_text = _parted(
|
|
380
|
+
_safe_str(self.val),
|
|
381
|
+
_formatted(prefix),
|
|
382
|
+
_raw_place(self.val, lowered, parted),
|
|
383
|
+
_raw_place(prefix, prefix_lowered, parted),
|
|
384
|
+
)
|
|
385
|
+
return self.error(
|
|
386
|
+
f"Expected <{actual_text}> to case-insensitive start with <{prefix_text}>, but did not."
|
|
387
|
+
f"{under(not_at_an_end(self.val, prefix, start=True, cased=False))}",
|
|
388
|
+
expected=prefix,
|
|
389
|
+
)
|
|
390
|
+
return self
|
|
391
|
+
|
|
392
|
+
def ends_with_ignoring_case(self, suffix: str) -> Self:
|
|
393
|
+
"""Asserts that val is a string and ends with suffix, ignoring case.
|
|
394
|
+
|
|
395
|
+
Like [`ends_with()`][assertpy.string.StringMixin.ends_with] but case-insensitive
|
|
396
|
+
(via ``str.lower()``), and strings only.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
suffix: the suffix
|
|
400
|
+
|
|
401
|
+
Examples:
|
|
402
|
+
Usage:
|
|
403
|
+
|
|
404
|
+
assert_that('FooBar').ends_with_ignoring_case('BAR')
|
|
405
|
+
assert_that('foobar').ends_with_ignoring_case('Bar')
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
409
|
+
|
|
410
|
+
Raises:
|
|
411
|
+
AssertionError: if val does **not** case-insensitive end with suffix
|
|
412
|
+
TypeError: if val or the given suffix is not a string
|
|
413
|
+
ValueError: if the given suffix is empty
|
|
414
|
+
"""
|
|
415
|
+
require_type(self.val, str, "a string")
|
|
416
|
+
require_type(suffix, str, "a string", subject=argument("suffix"))
|
|
417
|
+
if len(suffix) == 0:
|
|
418
|
+
raise ValueError("given suffix arg must not be empty")
|
|
419
|
+
if not self.val.lower().endswith(suffix.lower()):
|
|
420
|
+
lowered, suffix_lowered = self.val.lower(), suffix.lower()
|
|
421
|
+
parted, suffix_parted = _parted_from_the_end(lowered, suffix_lowered)
|
|
422
|
+
actual_text, suffix_text = _parted(
|
|
423
|
+
_safe_str(self.val),
|
|
424
|
+
_formatted(suffix),
|
|
425
|
+
_raw_place(self.val, lowered, parted),
|
|
426
|
+
_raw_place(suffix, suffix_lowered, suffix_parted),
|
|
427
|
+
)
|
|
428
|
+
return self.error(
|
|
429
|
+
f"Expected <{actual_text}> to case-insensitive end with <{suffix_text}>, but did not."
|
|
430
|
+
f"{under(not_at_an_end(self.val, suffix, start=False, cased=False))}",
|
|
431
|
+
expected=suffix,
|
|
432
|
+
)
|
|
433
|
+
return self
|
|
434
|
+
|
|
435
|
+
def matches(self, pattern: str | re.Pattern[str]) -> Self:
|
|
436
|
+
"""Asserts that val is string and matches the given regex pattern.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
pattern: the regular expression pattern, as raw string (aka prefixed with ``r``) or compiled from one
|
|
440
|
+
with ``re.compile()``
|
|
441
|
+
|
|
442
|
+
Examples:
|
|
443
|
+
Usage:
|
|
444
|
+
|
|
445
|
+
assert_that('foo').matches(r'\\w')
|
|
446
|
+
assert_that('123-456-7890').matches(r'\\d{3}-\\d{3}-\\d{4}')
|
|
447
|
+
|
|
448
|
+
Match is partial unless anchored, so these assertion pass:
|
|
449
|
+
|
|
450
|
+
assert_that('foo').matches(r'\\w')
|
|
451
|
+
assert_that('foo').matches(r'oo')
|
|
452
|
+
assert_that('foo').matches(r'\\w{2}')
|
|
453
|
+
|
|
454
|
+
To match the entire string, anchor the pattern with ``\\A`` and ``\\Z``, which match the start and
|
|
455
|
+
end of the string. ``^`` and ``$`` match the start and end of a line, and ``$`` also matches before
|
|
456
|
+
a final newline, so ``^\\w{3}$`` passes on ``'foo\\n'`` too:
|
|
457
|
+
|
|
458
|
+
assert_that('foo').matches(r'\\A\\w{3}\\Z')
|
|
459
|
+
assert_that('foo\\n').matches(r'^\\w{3}$')
|
|
460
|
+
|
|
461
|
+
Regex flags, such as ``re.MULTILINE`` and ``re.DOTALL``, go in as *inline modifiers*, such as
|
|
462
|
+
``(?m)`` and ``(?s)``, or on a pattern compiled with them:
|
|
463
|
+
|
|
464
|
+
s = '''bar
|
|
465
|
+
foo
|
|
466
|
+
baz'''
|
|
467
|
+
|
|
468
|
+
# using multiline (?m)
|
|
469
|
+
assert_that(s).matches(r'(?m)^foo$')
|
|
470
|
+
|
|
471
|
+
# using dotall (?s)
|
|
472
|
+
assert_that(s).matches(r'(?s)b(.*)z')
|
|
473
|
+
|
|
474
|
+
# the same, compiled
|
|
475
|
+
assert_that(s).matches(re.compile(r'b(.*)z', re.DOTALL))
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
479
|
+
|
|
480
|
+
Raises:
|
|
481
|
+
AssertionError: if val does **not** match pattern
|
|
482
|
+
TypeError: if val is not a string, or pattern is neither a string nor a pattern compiled from one
|
|
483
|
+
ValueError: if pattern is empty
|
|
484
|
+
|
|
485
|
+
Tip:
|
|
486
|
+
Regular expressions are tricky. Be sure to use raw strings (aka prefixed with ``r``).
|
|
487
|
+
Also, note that the [`matches()`][assertpy.string.StringMixin.matches] assertion passes
|
|
488
|
+
when the pattern is found anywhere in the string (it calls ``re.search``). So, if you need
|
|
489
|
+
to match the entire string, you must include anchors in the regex pattern.
|
|
490
|
+
"""
|
|
491
|
+
require_type(self.val, str, "a string")
|
|
492
|
+
if require_pattern(pattern).search(self.val) is None:
|
|
493
|
+
return self.error(
|
|
494
|
+
f"Expected <{_capped(self.val)}> to match pattern <{_capped_format(pattern)}>, but did not.",
|
|
495
|
+
expected=pattern,
|
|
496
|
+
)
|
|
497
|
+
return self
|
|
498
|
+
|
|
499
|
+
def does_not_match(self, pattern: str | re.Pattern[str]) -> Self:
|
|
500
|
+
"""Asserts that val is string and does not match the given regex pattern.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
pattern: the regular expression pattern, as raw string (aka prefixed with ``r``) or compiled from one
|
|
504
|
+
with ``re.compile()``
|
|
505
|
+
|
|
506
|
+
Examples:
|
|
507
|
+
Usage:
|
|
508
|
+
|
|
509
|
+
assert_that('foo').does_not_match(r'\\d+')
|
|
510
|
+
assert_that('123').does_not_match(r'\\w+')
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
514
|
+
|
|
515
|
+
Raises:
|
|
516
|
+
AssertionError: if val **does** match pattern
|
|
517
|
+
TypeError: if val is not a string, or pattern is neither a string nor a pattern compiled from one
|
|
518
|
+
ValueError: if pattern is empty
|
|
519
|
+
|
|
520
|
+
See Also:
|
|
521
|
+
[`matches()`][assertpy.string.StringMixin.matches] - for more about regex patterns
|
|
522
|
+
"""
|
|
523
|
+
require_type(self.val, str, "a string")
|
|
524
|
+
if require_pattern(pattern).search(self.val) is not None:
|
|
525
|
+
return self.error(
|
|
526
|
+
f"Expected <{_capped(self.val)}> to not match pattern <{_capped_format(pattern)}>, but did."
|
|
527
|
+
)
|
|
528
|
+
return self
|
|
529
|
+
|
|
530
|
+
def is_alpha(self) -> Self:
|
|
531
|
+
"""Asserts that val is non-empty string and all characters are alphabetic (using ``str.isalpha()``).
|
|
532
|
+
|
|
533
|
+
Examples:
|
|
534
|
+
Usage:
|
|
535
|
+
|
|
536
|
+
assert_that('foo').is_alpha()
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
540
|
+
|
|
541
|
+
Raises:
|
|
542
|
+
AssertionError: if val is **not** alphabetic
|
|
543
|
+
"""
|
|
544
|
+
return self._assert_string_chars(str.isalpha, "alphabetic chars")
|
|
545
|
+
|
|
546
|
+
def is_digit(self) -> Self:
|
|
547
|
+
"""Asserts that val is non-empty string and all characters are digits (using ``str.isdigit()``).
|
|
548
|
+
|
|
549
|
+
Examples:
|
|
550
|
+
Usage:
|
|
551
|
+
|
|
552
|
+
assert_that('1234567890').is_digit()
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
556
|
+
|
|
557
|
+
Raises:
|
|
558
|
+
AssertionError: if val is **not** digits
|
|
559
|
+
"""
|
|
560
|
+
return self._assert_string_chars(str.isdigit, "digits")
|
|
561
|
+
|
|
562
|
+
def is_lower(self) -> Self:
|
|
563
|
+
"""Asserts that val is non-empty string and all characters are lowercase (using ``str.lower()``).
|
|
564
|
+
|
|
565
|
+
Examples:
|
|
566
|
+
Usage:
|
|
567
|
+
|
|
568
|
+
assert_that('foo').is_lower()
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
572
|
+
|
|
573
|
+
Raises:
|
|
574
|
+
AssertionError: if val is **not** lowercase
|
|
575
|
+
"""
|
|
576
|
+
return self._assert_string_chars(lambda value: value == value.lower(), "lowercase chars")
|
|
577
|
+
|
|
578
|
+
def is_upper(self) -> Self:
|
|
579
|
+
"""Asserts that val is non-empty string and all characters are uppercase (using ``str.upper()``).
|
|
580
|
+
|
|
581
|
+
Examples:
|
|
582
|
+
Usage:
|
|
583
|
+
|
|
584
|
+
assert_that('FOO').is_upper()
|
|
585
|
+
|
|
586
|
+
Returns:
|
|
587
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
588
|
+
|
|
589
|
+
Raises:
|
|
590
|
+
AssertionError: if val is **not** uppercase
|
|
591
|
+
"""
|
|
592
|
+
return self._assert_string_chars(lambda value: value == value.upper(), "uppercase chars")
|
|
593
|
+
|
|
594
|
+
def is_alphanumeric(self) -> Self:
|
|
595
|
+
"""Asserts that val is non-empty string and all characters are alphanumeric (using ``str.isalnum()``).
|
|
596
|
+
|
|
597
|
+
Examples:
|
|
598
|
+
Usage:
|
|
599
|
+
|
|
600
|
+
assert_that('abc123').is_alphanumeric()
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
604
|
+
|
|
605
|
+
Raises:
|
|
606
|
+
AssertionError: if val is **not** alphanumeric
|
|
607
|
+
"""
|
|
608
|
+
return self._assert_string_chars(str.isalnum, "alphanumeric chars")
|
|
609
|
+
|
|
610
|
+
def is_whitespace(self) -> Self:
|
|
611
|
+
"""Asserts that val is non-empty string and all characters are whitespace (using ``str.isspace()``).
|
|
612
|
+
|
|
613
|
+
Examples:
|
|
614
|
+
Usage:
|
|
615
|
+
|
|
616
|
+
assert_that(' ').is_whitespace()
|
|
617
|
+
assert_that('\\t\\n').is_whitespace()
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
621
|
+
|
|
622
|
+
Raises:
|
|
623
|
+
AssertionError: if val is **not** whitespace
|
|
624
|
+
"""
|
|
625
|
+
return self._assert_string_chars(str.isspace, "whitespace")
|
|
626
|
+
|
|
627
|
+
def contains_any_of(self, *items: str) -> Self:
|
|
628
|
+
"""Asserts that val is a string and contains at least one of the given items.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
*items: the items, at least one of which is expected to be contained
|
|
632
|
+
|
|
633
|
+
Examples:
|
|
634
|
+
Usage:
|
|
635
|
+
|
|
636
|
+
assert_that('foobar').contains_any_of('foo', 'xxx')
|
|
637
|
+
assert_that('foobar').contains_any_of('xxx', 'bar')
|
|
638
|
+
|
|
639
|
+
Returns:
|
|
640
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
641
|
+
|
|
642
|
+
Raises:
|
|
643
|
+
AssertionError: if val does **not** contain any of the items
|
|
644
|
+
"""
|
|
645
|
+
require_type(self.val, str, "a string")
|
|
646
|
+
if len(items) == 0:
|
|
647
|
+
raise ValueError("one or more args must be given")
|
|
648
|
+
for item in items:
|
|
649
|
+
require_type(item, str, "a string", subject=argument("item"))
|
|
650
|
+
if not any(item in self.val for item in items):
|
|
651
|
+
return self.error(
|
|
652
|
+
f"Expected <{_capped(self.val)}> to contain any of {self._fmt_items(items)}, but did not.",
|
|
653
|
+
expected=items,
|
|
654
|
+
)
|
|
655
|
+
return self
|
|
656
|
+
|
|
657
|
+
def contains_none_of(self, *items: str) -> Self:
|
|
658
|
+
"""Asserts that val is a string and contains none of the given items.
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
*items: the items, none of which should be contained
|
|
662
|
+
|
|
663
|
+
Examples:
|
|
664
|
+
Usage:
|
|
665
|
+
|
|
666
|
+
assert_that('foobar').contains_none_of('xxx', 'yyy')
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
670
|
+
|
|
671
|
+
Raises:
|
|
672
|
+
AssertionError: if val **does** contain any of the items
|
|
673
|
+
"""
|
|
674
|
+
require_type(self.val, str, "a string")
|
|
675
|
+
if len(items) == 0:
|
|
676
|
+
raise ValueError("one or more args must be given")
|
|
677
|
+
for item in items:
|
|
678
|
+
require_type(item, str, "a string", subject=argument("item"))
|
|
679
|
+
found = [item for item in items if item in self.val]
|
|
680
|
+
if found:
|
|
681
|
+
return self.error(
|
|
682
|
+
f"Expected <{_capped(self.val)}> to contain none of {self._fmt_items(items)},"
|
|
683
|
+
f" but did contain {self._fmt_items(found)}."
|
|
684
|
+
)
|
|
685
|
+
return self
|
|
686
|
+
|
|
687
|
+
def is_unicode(self) -> Self:
|
|
688
|
+
"""Asserts that val is a ``str``.
|
|
689
|
+
|
|
690
|
+
Retained for ``assertpy`` compatibility: every ``str`` is unicode on Python 3, so this is
|
|
691
|
+
effectively an ``isinstance(val, str)`` check.
|
|
692
|
+
|
|
693
|
+
Examples:
|
|
694
|
+
Usage:
|
|
695
|
+
|
|
696
|
+
assert_that('foo').is_unicode()
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
AssertionBuilder: returns this instance to chain to the next assertion
|
|
700
|
+
|
|
701
|
+
Raises:
|
|
702
|
+
AssertionError: if val is **not** a ``str``
|
|
703
|
+
"""
|
|
704
|
+
if not isinstance(self.val, str):
|
|
705
|
+
return self.error(f"Expected <{_capped(self.val)}> to be unicode, but was <{type(self.val).__name__}>.")
|
|
706
|
+
return self
|
|
707
|
+
|
|
708
|
+
def extracting_group(self, pattern: str | re.Pattern[str], group: int | str = 0) -> Self:
|
|
709
|
+
"""Search val for ``pattern`` and return a new builder whose val is the captured group.
|
|
710
|
+
|
|
711
|
+
A group the pattern does not have fails it with or without ``not_``: that is a mistake in the call,
|
|
712
|
+
found once the pattern matched, and not an answer about val.
|
|
713
|
+
|
|
714
|
+
Args:
|
|
715
|
+
pattern: the regular expression pattern, as a string or compiled from one
|
|
716
|
+
group: the group index (int) or name (str) to extract. Defaults to ``0``
|
|
717
|
+
(the entire match).
|
|
718
|
+
|
|
719
|
+
Examples:
|
|
720
|
+
Usage with positional groups:
|
|
721
|
+
|
|
722
|
+
assert_that("status=200 path=/api").extracting_group(r"status=(\\d+)", 1).is_equal_to("200")
|
|
723
|
+
|
|
724
|
+
Usage with named groups:
|
|
725
|
+
|
|
726
|
+
assert_that("2024-01-15 ERROR").extracting_group(
|
|
727
|
+
r"(?P<level>\\w+)$", "level"
|
|
728
|
+
).is_equal_to("ERROR")
|
|
729
|
+
|
|
730
|
+
Returns:
|
|
731
|
+
AssertionBuilder: a **new** builder whose val is the extracted group string
|
|
732
|
+
|
|
733
|
+
Raises:
|
|
734
|
+
TypeError: if val is not a string, or pattern is neither a string nor a pattern compiled from one
|
|
735
|
+
ValueError: if pattern is empty
|
|
736
|
+
AssertionError: if the pattern does not match val or the group does not exist
|
|
737
|
+
"""
|
|
738
|
+
require_type(self.val, str, "a string")
|
|
739
|
+
match_obj = require_pattern(pattern).search(self.val)
|
|
740
|
+
if match_obj is None:
|
|
741
|
+
return self.error(
|
|
742
|
+
f"Expected <{_capped(self.val)}> to match pattern <{_capped_format(pattern)}>, but did not.",
|
|
743
|
+
expected=pattern,
|
|
744
|
+
)
|
|
745
|
+
try:
|
|
746
|
+
extracted = match_obj.group(group)
|
|
747
|
+
except IndexError:
|
|
748
|
+
self._unmet(
|
|
749
|
+
f"Expected pattern <{_capped_format(pattern)}> to have group <{_capped_format(group)}>,"
|
|
750
|
+
" but it does not.",
|
|
751
|
+
suppress_context=True,
|
|
752
|
+
expected=pattern,
|
|
753
|
+
)
|
|
754
|
+
return self
|
|
755
|
+
if extracted is None:
|
|
756
|
+
return self.error(
|
|
757
|
+
f"Expected group <{_capped_format(group)}> of pattern <{_capped_format(pattern)}> to be matched in"
|
|
758
|
+
f" <{_capped(self.val)}>, but it was not.",
|
|
759
|
+
expected=pattern,
|
|
760
|
+
)
|
|
761
|
+
return self.builder(extracted, self.description, self.kind, logger=self.logger)
|
|
762
|
+
|
|
763
|
+
def matches_with_groups(self, pattern: str | re.Pattern[str]) -> Self:
|
|
764
|
+
"""Search val for ``pattern`` and return a new builder whose val is the tuple of all groups.
|
|
765
|
+
|
|
766
|
+
If the pattern contains **named** groups, the builder val is a ``dict``
|
|
767
|
+
of ``{name: value}`` for all named groups. Otherwise it is the
|
|
768
|
+
``tuple`` returned by ``Match.groups()``, empty for a pattern without groups.
|
|
769
|
+
|
|
770
|
+
Args:
|
|
771
|
+
pattern: the regular expression pattern, as a string or compiled from one
|
|
772
|
+
|
|
773
|
+
Examples:
|
|
774
|
+
Positional groups:
|
|
775
|
+
|
|
776
|
+
assert_that("2024-01-15 ERROR").matches_with_groups(
|
|
777
|
+
r"(\\d{4}-\\d{2}-\\d{2}) (\\w+)"
|
|
778
|
+
).is_length(2)
|
|
779
|
+
|
|
780
|
+
Named groups:
|
|
781
|
+
|
|
782
|
+
assert_that("status=200").matches_with_groups(
|
|
783
|
+
r"(?P<key>\\w+)=(?P<val>\\w+)"
|
|
784
|
+
).contains_key("key").contains_key("val")
|
|
785
|
+
|
|
786
|
+
Returns:
|
|
787
|
+
AssertionBuilder: a **new** builder whose val is the groups tuple or groupdict
|
|
788
|
+
|
|
789
|
+
Raises:
|
|
790
|
+
TypeError: if val is not a string, or pattern is neither a string nor a pattern compiled from one
|
|
791
|
+
ValueError: if pattern is empty
|
|
792
|
+
AssertionError: if the pattern does not match val
|
|
793
|
+
"""
|
|
794
|
+
require_type(self.val, str, "a string")
|
|
795
|
+
match_obj = require_pattern(pattern).search(self.val)
|
|
796
|
+
if match_obj is None:
|
|
797
|
+
return self.error(
|
|
798
|
+
f"Expected <{_capped(self.val)}> to match pattern <{_capped_format(pattern)}>, but did not.",
|
|
799
|
+
expected=pattern,
|
|
800
|
+
)
|
|
801
|
+
groupdict = match_obj.groupdict()
|
|
802
|
+
result = groupdict or match_obj.groups()
|
|
803
|
+
return self.builder(result, self.description, self.kind, logger=self.logger)
|