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/collection.py ADDED
@@ -0,0 +1,528 @@
1
+ from __future__ import annotations
2
+
3
+ import collections.abc
4
+ import inspect
5
+ from typing import TYPE_CHECKING, Any, SupportsIndex, cast
6
+
7
+ from ._engine._introspection import is_mapping_like
8
+ from ._engine._membership import flattened_supersets, subset_faults
9
+ from ._engine._mixin_base import _MixinBase
10
+ from ._engine._ordering import (
11
+ UnorderableError,
12
+ first_out_of_order,
13
+ first_plainly_out_of_order,
14
+ plainly_sortable,
15
+ require_integer,
16
+ )
17
+ from ._engine._require import argument, refuse, require_type, sized_len, verdict
18
+ from ._hints import Roles
19
+ from ._satisfies import _warn_vacuous
20
+ from .contains import _one_not_found, _pair_why
21
+ from .errors import _capped, _capped_format
22
+ from .matchers import _is_matcher
23
+
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Callable, Iterable, Sized
26
+
27
+ from ._engine._compat import Self
28
+ from ._engine._introspection import MappingLike
29
+ from .matchers import Matcher
30
+
31
+ __tracebackhide__ = True
32
+
33
+
34
+ class CollectionMixin(_MixinBase):
35
+ """Collection assertions mixin."""
36
+
37
+ def _as_list(self) -> list[Any]:
38
+ """Returns val as a list, raising TypeError if val is not iterable."""
39
+ require_type(self.val, collections.abc.Iterable, "iterable")
40
+ return list(self._walked())
41
+
42
+ def is_iterable(self) -> Self:
43
+ """Asserts that val is iterable.
44
+
45
+ Examples:
46
+ Usage:
47
+
48
+ assert_that('foo').is_iterable()
49
+ assert_that(['a', 'b']).is_iterable()
50
+ assert_that((1, 2, 3)).is_iterable()
51
+
52
+ Returns:
53
+ AssertionBuilder: returns this instance to chain to the next assertion
54
+
55
+ Raises:
56
+ AssertionError: if val is **not** iterable
57
+ """
58
+ if not isinstance(self.val, collections.abc.Iterable):
59
+ return self.error("Expected iterable, but was not.")
60
+ return self
61
+
62
+ def is_not_iterable(self) -> Self:
63
+ """Asserts that val is not iterable.
64
+
65
+ Examples:
66
+ Usage:
67
+
68
+ assert_that(1).is_not_iterable()
69
+ assert_that(123.4).is_not_iterable()
70
+ assert_that(True).is_not_iterable()
71
+ assert_that(None).is_not_iterable()
72
+
73
+ Returns:
74
+ AssertionBuilder: returns this instance to chain to the next assertion
75
+
76
+ Raises:
77
+ AssertionError: if val **is** iterable
78
+ """
79
+ if isinstance(self.val, collections.abc.Iterable):
80
+ return self.error("Expected not iterable, but was.")
81
+ return self
82
+
83
+ def is_subset_of(self, *supersets: object, allow_empty: bool = False) -> Self:
84
+ """Asserts that val is iterable and a subset of the given superset (or supersets).
85
+
86
+ Args:
87
+ *supersets (object): the expected superset (or supersets)
88
+
89
+ Examples:
90
+ Usage:
91
+
92
+ assert_that('foo').is_subset_of('abcdefghijklmnopqrstuvwxyz')
93
+ assert_that(['a', 'b']).is_subset_of(['a', 'b', 'c'])
94
+ assert_that((1, 2, 3)).is_subset_of([1, 2, 3, 4])
95
+ assert_that({'a': 1, 'b': 2}).is_subset_of({'a': 1, 'b': 2, 'c': 3})
96
+ assert_that({'a', 'b'}).is_subset_of({'a', 'b', 'c'})
97
+
98
+ # or multiple supersets (as comma-separated args)
99
+ assert_that('aBc').is_subset_of('abc', 'ABC')
100
+ assert_that((1, 2, 3)).is_subset_of([1, 3, 5], [2, 4, 6])
101
+
102
+ assert_that({'a': 1, 'b': 2}).is_subset_of({'a': 1, 'c': 3}) # fails
103
+ # Expected <{'a': 1, 'b': 2}> to be subset of <{'a': 1, 'c': 3}>, but <{'b': 2}> was missing.
104
+
105
+ Returns:
106
+ AssertionBuilder: returns this instance to chain to the next assertion
107
+
108
+ Raises:
109
+ AssertionError: if val is **not** subset of given superset (or supersets)
110
+
111
+ Note:
112
+ Where one element of val is not found, the failure says what is nearest to it: the closest row among
113
+ the items of the superset, and on a line under the message why it is not there, where one fact about
114
+ it says so.
115
+ """
116
+ require_type(self.val, collections.abc.Iterable, "iterable")
117
+ if len(supersets) == 0:
118
+ raise ValueError("one or more superset args must be given")
119
+
120
+ missing = []
121
+ if is_mapping_like(self.val):
122
+ # read whole first: a superset sharing this iterator leaves nothing, and a subset of nothing is anything
123
+ entries = [(key, self.val[key]) for key in self.val]
124
+ # each read once and kept apart: merging printed a value under a key no superset held it under
125
+ subject = dict(entries)
126
+ read = []
127
+ for superset_index, superset in enumerate(supersets):
128
+ self._require_dict_like(superset, check_values=False, name=f"arg #{superset_index + 1}")
129
+ mapping = cast("MappingLike", superset)
130
+ # a one-shot subject given as its own superset has nothing left to read, and is its own subset
131
+ read.append(subject if superset is self.val else {key: mapping[key] for key in mapping})
132
+ missing.extend(subset_faults(subject, read))
133
+ searched_in = read[0] if len(read) == 1 else read
134
+ if missing:
135
+ return self.error(
136
+ f"Expected <{_capped(self.val)}> to be subset of <{_capped_format(searched_in)}>, "
137
+ f"but {self._fmt_items(missing)} {'was' if len(missing) == 1 else 'were'} missing."
138
+ + _pair_why(missing, read),
139
+ expected=searched_in,
140
+ )
141
+ if not entries:
142
+ _warn_vacuous(self, "is_subset_of", allow_empty)
143
+ else:
144
+ walked = list(self._walked())
145
+ # flattened once: a one-shot superset is drained by the first pass and reads empty to a second
146
+ collected = flattened_supersets(supersets)
147
+ # the same core the matcher uses: a bare `set()` called a value whose hash disagrees with `==` missing
148
+ missing.extend(subset_faults(walked, (collected,)))
149
+ try:
150
+ # for the message only: the failure has always shown the superset as a set where that was possible
151
+ superset_values: object = set(collected)
152
+ except TypeError:
153
+ superset_values = collected
154
+ if missing:
155
+ roles = Roles("the item missing", "an item of the superset")
156
+ return self.error(
157
+ f"Expected <{_capped(self.val)}> to be subset of {self._fmt_items(superset_values)}, "
158
+ f"but {self._fmt_items(missing)} {'was' if len(missing) == 1 else 'were'} missing."
159
+ + _one_not_found(missing, collected, noun="item", item_is_actual=True, roles=roles),
160
+ expected=superset_values,
161
+ )
162
+ if not walked:
163
+ _warn_vacuous(self, "is_subset_of", allow_empty)
164
+
165
+ return self
166
+
167
+ def is_sorted(
168
+ self,
169
+ key: Callable[[Any], Any] = lambda item: item,
170
+ reverse: bool = False,
171
+ *,
172
+ allow_empty: bool = False,
173
+ ) -> Self:
174
+ """Asserts that val is iterable and is sorted.
175
+
176
+ Args:
177
+ key (function): the one-arg function to extract the sort comparison key. Defaults to
178
+ ``lambda x: x`` to just compare items directly.
179
+ reverse (bool): if ``True``, then comparison key is reversed. Defaults to ``False``.
180
+
181
+ Examples:
182
+ Usage:
183
+
184
+ assert_that(['a', 'b', 'c']).is_sorted()
185
+ assert_that((1, 2, 3)).is_sorted()
186
+
187
+ # with a key function
188
+ assert_that('aBc').is_sorted(key=str.lower)
189
+
190
+ # reverse order
191
+ assert_that(['c', 'b', 'a']).is_sorted(reverse=True)
192
+ assert_that((3, 2, 1)).is_sorted(reverse=True)
193
+
194
+ assert_that((1, 2, 3, 4, -5, 6)).is_sorted() # fails
195
+ # Expected <(1, 2, 3, 4, -5, 6)> to be sorted, but subset <4, -5> at index 3 is not.
196
+
197
+ Returns:
198
+ AssertionBuilder: returns this instance to chain to the next assertion
199
+
200
+ Raises:
201
+ AssertionError: if val is **not** sorted
202
+ """
203
+ require_type(self.val, collections.abc.Iterable, "iterable")
204
+ if not callable(key):
205
+ refuse(key, "callable", subject=argument("key"))
206
+
207
+ walked = 0
208
+
209
+ def _counted(items):
210
+ nonlocal walked
211
+ for item in items:
212
+ walked += 1
213
+ yield item
214
+
215
+ broken = None
216
+ try:
217
+ if key is _UNKEYED and plainly_sortable(self.val):
218
+ walked = len(self.val)
219
+ broken = first_plainly_out_of_order(self.val, reverse=reverse)
220
+ else:
221
+ broken = first_out_of_order(_counted(self._walked()), key=key, reverse=reverse)
222
+ except UnorderableError:
223
+ # reported about the collection: Python's own message is about the operator and names neither side
224
+ unorderable = True
225
+ else:
226
+ unorderable = False
227
+ if unorderable:
228
+ refuse(self.val, "a collection whose items can be ordered against each other")
229
+ if broken is not None:
230
+ index, earlier, later = broken
231
+ direction = " reverse" if reverse else ""
232
+ return self.error(
233
+ f"Expected <{_capped(self.val)}> to be sorted{direction}, "
234
+ f"but subset {self._fmt_items([earlier, later])} at index {index} is not."
235
+ )
236
+ if not walked:
237
+ _warn_vacuous(self, "is_sorted", allow_empty)
238
+ return self
239
+
240
+ def has_same_size_as(self, other: Sized) -> Self:
241
+ """Asserts that val has the same length as other.
242
+
243
+ Args:
244
+ other (object): a sized object whose ``len()`` is compared with val's
245
+
246
+ Examples:
247
+ Usage:
248
+
249
+ assert_that([1, 2, 3]).has_same_size_as((4, 5, 6))
250
+ assert_that('foo').has_same_size_as([1, 2, 3])
251
+
252
+ Returns:
253
+ AssertionBuilder: returns this instance to chain to the next assertion
254
+
255
+ Raises:
256
+ AssertionError: if val and other do **not** have the same length
257
+ TypeError: if other is not a sized object
258
+ """
259
+ # the subject first: reading the operand first let the operand's own `__len__` answer for both
260
+ actual_len = sized_len(self.val)
261
+ # `other` was left to `len()`, whose message is about the builtin and names neither operand
262
+ other_len = sized_len(other, subject=argument("other"))
263
+ if actual_len != other_len:
264
+ return self.error(
265
+ f"Expected <{_capped(self.val)}> to have same size as <{_capped_format(other)}>"
266
+ f" of length <{other_len}>, but was length <{actual_len}>.",
267
+ expected=other_len,
268
+ )
269
+ return self
270
+
271
+ def has_size_greater_than(self, size: SupportsIndex) -> Self:
272
+ """Asserts that val has a length strictly greater than the given size.
273
+
274
+ Args:
275
+ size: the size val's length must exceed
276
+
277
+ Examples:
278
+ Usage:
279
+
280
+ assert_that([1, 2, 3]).has_size_greater_than(2)
281
+ assert_that('foo').has_size_greater_than(1)
282
+ assert_that({'a': 1, 'b': 2}).has_size_greater_than(1)
283
+
284
+ Returns:
285
+ AssertionBuilder: returns this instance to chain to the next assertion
286
+
287
+ Raises:
288
+ AssertionError: if val's length is **not** greater than the given size
289
+ TypeError: if the given arg is not an int
290
+ ValueError: if the given arg is negative
291
+ """
292
+ bound = size if type(size) is int else require_integer(size, "size")
293
+ if bound < 0:
294
+ raise ValueError("given arg must be a positive int")
295
+ actual = sized_len(self.val)
296
+ if actual <= bound:
297
+ return self.error(
298
+ f"Expected <{_capped(self.val)}> to have size greater than <{size}>, but was <{actual}>.",
299
+ expected=size,
300
+ )
301
+ return self
302
+
303
+ def has_size_less_than(self, size: SupportsIndex) -> Self:
304
+ """Asserts that val has a length strictly less than the given size.
305
+
306
+ Args:
307
+ size: the size val's length must stay under
308
+
309
+ Examples:
310
+ Usage:
311
+
312
+ assert_that([1, 2, 3]).has_size_less_than(4)
313
+ assert_that('foo').has_size_less_than(5)
314
+ assert_that({'a': 1, 'b': 2}).has_size_less_than(3)
315
+
316
+ Returns:
317
+ AssertionBuilder: returns this instance to chain to the next assertion
318
+
319
+ Raises:
320
+ AssertionError: if val's length is **not** less than the given size
321
+ TypeError: if the given arg is not an int
322
+ ValueError: if the given arg is negative
323
+ """
324
+ bound = size if type(size) is int else require_integer(size, "size")
325
+ if bound < 0:
326
+ raise ValueError("given arg must be a positive int")
327
+ actual = sized_len(self.val)
328
+ if actual >= bound:
329
+ return self.error(
330
+ f"Expected <{_capped(self.val)}> to have size less than <{size}>, but was <{actual}>.", expected=size
331
+ )
332
+ return self
333
+
334
+ def has_size_between(self, low: SupportsIndex, high: SupportsIndex) -> Self:
335
+ """Asserts that val has a length between low and high (both inclusive).
336
+
337
+ Args:
338
+ low: the inclusive lower size bound
339
+ high: the inclusive upper size bound
340
+
341
+ Examples:
342
+ Usage:
343
+
344
+ assert_that([1, 2, 3]).has_size_between(1, 5)
345
+ assert_that('foo').has_size_between(3, 3)
346
+ assert_that({'a': 1, 'b': 2}).has_size_between(0, 2)
347
+
348
+ Returns:
349
+ AssertionBuilder: returns this instance to chain to the next assertion
350
+
351
+ Raises:
352
+ AssertionError: if val's length is **not** between low and high
353
+ TypeError: if a given arg is not an int
354
+ ValueError: if a given arg is negative, or low is greater than high
355
+ """
356
+ least = low if type(low) is int else require_integer(low, "low")
357
+ most = high if type(high) is int else require_integer(high, "high")
358
+ if least < 0 or most < 0:
359
+ raise ValueError("given args must be positive ints")
360
+ if least > most:
361
+ raise ValueError("given low arg must be less than given high arg")
362
+ if not least <= sized_len(self.val) <= most:
363
+ return self.error(
364
+ f"Expected <{_capped(self.val)}> to have size between <{low}> "
365
+ f"and <{high}>, but was <{sized_len(self.val)}>.",
366
+ expected=(low, high),
367
+ )
368
+ return self
369
+
370
+ def filtered_on(self, predicate: Matcher[Any] | Callable[[Any], bool]) -> Self:
371
+ """Returns a new builder with elements matching the predicate.
372
+
373
+ Args:
374
+ predicate: callable or Matcher. If a Matcher, uses ``predicate.matches(item)``.
375
+
376
+ Examples:
377
+ Usage:
378
+
379
+ assert_that([1, -2, 3]).filtered_on(lambda x: x > 0).is_length(2)
380
+ assert_that(items).filtered_on(match.is_positive()).is_not_empty()
381
+
382
+ Returns:
383
+ AssertionBuilder: returns a new instance with the filtered list as val
384
+ """
385
+ require_type(self.val, collections.abc.Iterable, "iterable")
386
+ # the protocol test the other matcher-taking methods use: a duck-typed matcher was called as a function
387
+ matches = predicate.matches if _is_matcher(predicate) else cast("Callable[..., object]", predicate)
388
+ # counted in the pass: a generator is spent by it, and asking after made an emptied source read as empty
389
+ seen = 0
390
+ filtered = []
391
+ for item in self._walked():
392
+ seen += 1
393
+ if verdict(matches(item)):
394
+ filtered.append(item)
395
+ return self.builder(
396
+ filtered,
397
+ self.description,
398
+ self.kind,
399
+ logger=self.logger,
400
+ origin=f"filtered_on() kept {len(filtered)} of {seen} items",
401
+ )
402
+
403
+ def mapped(self, func: Callable[[Any], Any]) -> Self:
404
+ """Returns a new builder with each element transformed by func.
405
+
406
+ Args:
407
+ func: callable applied to each element.
408
+
409
+ Examples:
410
+ Usage:
411
+
412
+ assert_that(["a", "b"]).mapped(str.upper).contains("A")
413
+
414
+ Returns:
415
+ AssertionBuilder: returns a new instance with the mapped list as val
416
+ """
417
+ require_type(self.val, collections.abc.Iterable, "iterable")
418
+ mapped = [func(item) for item in self._walked()]
419
+ return self.builder(mapped, self.description, self.kind, logger=self.logger)
420
+
421
+ def flat_mapped(self, func: Callable[[Any], Iterable[Any]]) -> Self:
422
+ """Returns a new builder with each element expanded and flattened by func.
423
+
424
+ Args:
425
+ func: callable returning an iterable for each element.
426
+
427
+ Examples:
428
+ Usage:
429
+
430
+ assert_that(["ab", "cd"]).flat_mapped(list).contains("a", "c")
431
+
432
+ Returns:
433
+ AssertionBuilder: returns a new instance with the flattened list as val
434
+ """
435
+ require_type(self.val, collections.abc.Iterable, "iterable")
436
+ walked, flat = self._walked(), []
437
+ for item in walked:
438
+ made = func(item)
439
+ # the value itself handed back is read as every link reads it: taken from under them, it left the
440
+ # next link the first item alone
441
+ flat.extend(walked if made is self.val else made)
442
+ return self.builder(flat, self.description, self.kind, logger=self.logger)
443
+
444
+ def first(self) -> Self:
445
+ """Returns a new builder with the first element of val.
446
+
447
+ Examples:
448
+ Usage:
449
+
450
+ assert_that([10, 20, 30]).first().is_equal_to(10)
451
+
452
+ Returns:
453
+ AssertionBuilder: returns a new instance with the first element as val
454
+
455
+ Raises:
456
+ ValueError: if val is empty
457
+ """
458
+ items = self._as_list()
459
+ if not items:
460
+ raise ValueError("Expected non-empty iterable, but was empty.")
461
+ return self.builder(items[0], self.description, self.kind, logger=self.logger)
462
+
463
+ def last(self) -> Self:
464
+ """Returns a new builder with the last element of val.
465
+
466
+ Examples:
467
+ Usage:
468
+
469
+ assert_that([10, 20, 30]).last().is_equal_to(30)
470
+
471
+ Returns:
472
+ AssertionBuilder: returns a new instance with the last element as val
473
+
474
+ Raises:
475
+ ValueError: if val is empty
476
+ """
477
+ items = self._as_list()
478
+ if not items:
479
+ raise ValueError("Expected non-empty iterable, but was empty.")
480
+ return self.builder(items[-1], self.description, self.kind, logger=self.logger)
481
+
482
+ def element(self, index: SupportsIndex) -> Self:
483
+ """Returns a new builder with the element at the given index.
484
+
485
+ Args:
486
+ index: zero-based index.
487
+
488
+ Examples:
489
+ Usage:
490
+
491
+ assert_that([10, 20, 30]).element(1).is_equal_to(20)
492
+
493
+ Returns:
494
+ AssertionBuilder: returns a new instance with the selected element as val
495
+
496
+ Raises:
497
+ IndexError: if index is out of range
498
+ """
499
+ position = index if type(index) is int else require_integer(index, "index")
500
+ items = self._as_list()
501
+ if position < 0 or position >= len(items):
502
+ raise IndexError(f"Expected index {index} to be in range [0, {len(items)}), but was out of range.")
503
+ return self.builder(items[position], self.description, self.kind, logger=self.logger)
504
+
505
+ def single(self) -> Self:
506
+ """Returns a new builder with the only element of val.
507
+
508
+ Examples:
509
+ Usage:
510
+
511
+ assert_that([42]).single().is_equal_to(42)
512
+
513
+ Returns:
514
+ AssertionBuilder: returns a new instance with the single element as val
515
+
516
+ Raises:
517
+ ValueError: if val is empty or has more than one element
518
+ """
519
+ items = self._as_list()
520
+ if not items:
521
+ raise ValueError("Expected iterable with single element, but was empty.")
522
+ if len(items) > 1:
523
+ raise ValueError(f"Expected iterable with single element, but had {len(items)} elements.")
524
+ return self.builder(items[0], self.description, self.kind, logger=self.logger)
525
+
526
+
527
+ _UNKEYED = inspect.signature(CollectionMixin.is_sorted).parameters["key"].default
528
+ """`is_sorted` without a `key`, which asks nothing of anybody's code and so may compare plain items in C."""