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