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/numeric.py ADDED
@@ -0,0 +1,696 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import numbers
5
+ from typing import TYPE_CHECKING, SupportsFloat, SupportsIndex
6
+
7
+ from ._engine._compare import _is_infinite, _is_nan, within_either
8
+ from ._engine._mixin_base import _MixinBase
9
+ from ._engine._ordering import UnorderableError, compare, holds, require_integer
10
+ from ._engine._require import _shown, argument, refuse, require_type
11
+ from ._hints import apart_in_time, out_of_order, under
12
+ from .errors import _capped, _capped_format
13
+
14
+ if TYPE_CHECKING:
15
+ from ._engine._compat import Self
16
+
17
+ __tracebackhide__ = True
18
+
19
+
20
+ def _fmt_tolerance(tolerance: datetime.timedelta) -> str:
21
+ """A timedelta tolerance as ``h:mm:ss``, its hours counted on past a day, with its fraction where it has one."""
22
+ hours, remainder = divmod(tolerance.days * 86400 + tolerance.seconds, 3600)
23
+ minutes, seconds = divmod(remainder, 60)
24
+ fraction = f".{tolerance.microseconds:06d}" if tolerance.microseconds else ""
25
+ return f"{hours}:{minutes:02d}:{seconds:02d}{fraction}"
26
+
27
+
28
+ def _allowance(tolerance: object, rel: object) -> str:
29
+ """The tolerances a closeness was asked within, in the words of its message."""
30
+ relative = f"relative tolerance <{_capped_format(rel)}>"
31
+ if tolerance is None:
32
+ return relative
33
+ absolute = f"tolerance <{_capped_format(tolerance)}>"
34
+ return absolute if rel is None else f"{absolute} or {relative}"
35
+
36
+
37
+ class NumericMixin(_MixinBase):
38
+ """Numeric assertions mixin."""
39
+
40
+ _NUMERIC_COMPAREABLE = frozenset({datetime.datetime, datetime.timedelta, datetime.date, datetime.time})
41
+ _NUMERIC_NON_COMPAREABLE = frozenset({complex})
42
+
43
+ def _validate_compareable(self, other):
44
+ """Refuse the pair when it cannot be ordered, in the wording this surface uses.
45
+
46
+ Whether it can be is decided in `_engine._ordering`, which the matchers ask as well: the rule
47
+ used to be written once here as a list of types and once there as a `try`/`except`, and one
48
+ relation spelled twice is how the text matchers drifted apart on bytes.
49
+ """
50
+ try:
51
+ compare(self.val, other)
52
+ except UnorderableError as unordered:
53
+ self._refuse_unordered(unordered, other, "other")
54
+
55
+ def _refuse_unordered(self, unordered, other, name):
56
+ """The refusal for a pair the ordering engine could not order, naming the argument it came from."""
57
+ if unordered.kind == "value":
58
+ refuse(self.val, "a value with an ordering (complex numbers have none)")
59
+ kind_bound = unordered.wanted
60
+ if unordered.kind == "kind":
61
+ wanted = (
62
+ "a number"
63
+ if kind_bound is numbers.Number or kind_bound is None
64
+ else f"a {kind_bound.__name__}, to match val"
65
+ )
66
+ refuse(other, wanted, subject=argument(name))
67
+ # the pair cannot be ordered, not the value: two strings compare fine, so that was untrue of `"10" > 5`
68
+ refuse(other, f"comparable with val {_shown(self.val)}", subject=argument(name))
69
+
70
+ def _within(self, low, high, *, named=None):
71
+ """Whether ``low <= val <= high``, asked through the ordering engine so a NaN answers rather than signals.
72
+
73
+ A pair the engine cannot order is refused where it is met, in the words the single relations use,
74
+ and only there: below the low bound the answer is known, and the high one is never asked. Asked
75
+ up front instead, a high bound the value could not be ordered against turned a passing
76
+ `is_not_between` into a refusal.
77
+
78
+ *named* is what a refusal names, as ``(argument, its name)``: a window's bounds are worked out from
79
+ `other`, and `other` is what the caller wrote.
80
+ """
81
+ return self._holds(low, "ge", named or (low, "low")) and self._holds(high, "le", named or (high, "high"))
82
+
83
+ def _close(self, other, tolerance, rel):
84
+ """Whether val is within *tolerance* or *rel* of *other*, *rel* being a fraction of the size of *other*.
85
+
86
+ A pair the ordering engine cannot order is refused, in the words the single relations use.
87
+ """
88
+ try:
89
+ return within_either(self.val, other, tolerance, rel)
90
+ except UnorderableError as unordered:
91
+ self._refuse_unordered(unordered, other, "other")
92
+
93
+ def _holds(self, bound, relation, named):
94
+ """`holds` against one bound, with a pair the engine cannot order refused under *named*."""
95
+ try:
96
+ return holds(self.val, bound, relation)
97
+ except UnorderableError as unordered:
98
+ self._refuse_unordered(unordered, *named)
99
+
100
+ def _validate_number(self):
101
+ """Raise TypeError if val is not numeric."""
102
+ require_type(self.val, numbers.Number, "a number")
103
+
104
+ def _validate_real(self):
105
+ """Raise TypeError if val is not real number."""
106
+ require_type(self.val, numbers.Real, "a real number")
107
+
108
+ def is_zero(self) -> Self:
109
+ """Asserts that val is numeric and is zero.
110
+
111
+ Examples:
112
+ Usage:
113
+
114
+ assert_that(0).is_zero()
115
+
116
+ Returns:
117
+ AssertionBuilder: returns this instance to chain to the next assertion
118
+
119
+ Raises:
120
+ AssertionError: if val is **not** zero
121
+ """
122
+ self._validate_number()
123
+ return self.is_equal_to(0)
124
+
125
+ def is_not_zero(self) -> Self:
126
+ """Asserts that val is numeric and is *not* zero.
127
+
128
+ Examples:
129
+ Usage:
130
+
131
+ assert_that(1).is_not_zero()
132
+ assert_that(123.4).is_not_zero()
133
+
134
+ Returns:
135
+ AssertionBuilder: returns this instance to chain to the next assertion
136
+
137
+ Raises:
138
+ AssertionError: if val **is** zero
139
+ """
140
+ self._validate_number()
141
+ return self.is_not_equal_to(0)
142
+
143
+ def is_nan(self) -> Self:
144
+ """Asserts that val is real number and is ``NaN`` (not a number).
145
+
146
+ Examples:
147
+ Usage:
148
+
149
+ assert_that(float('nan')).is_nan()
150
+ assert_that(float('inf') * 0).is_nan()
151
+
152
+ Returns:
153
+ AssertionBuilder: returns this instance to chain to the next assertion
154
+
155
+ Raises:
156
+ AssertionError: if val is **not** NaN
157
+ """
158
+ self._validate_number()
159
+ self._validate_real()
160
+ if not _is_nan(self.val):
161
+ return self.error(f"Expected <{_capped(self.val)}> to be <NaN>, but was not.")
162
+ return self
163
+
164
+ def is_not_nan(self) -> Self:
165
+ """Asserts that val is real number and is *not* ``NaN`` (not a number).
166
+
167
+ Examples:
168
+ Usage:
169
+
170
+ assert_that(0).is_not_nan()
171
+ assert_that(123.4).is_not_nan()
172
+ assert_that(float('inf')).is_not_nan()
173
+
174
+ Returns:
175
+ AssertionBuilder: returns this instance to chain to the next assertion
176
+
177
+ Raises:
178
+ AssertionError: if val **is** NaN
179
+ """
180
+ self._validate_number()
181
+ self._validate_real()
182
+ if _is_nan(self.val):
183
+ return self.error("Expected not <NaN>, but was.")
184
+ return self
185
+
186
+ def is_inf(self) -> Self:
187
+ """Asserts that val is real number and is ``Inf`` (infinity).
188
+
189
+ Examples:
190
+ Usage:
191
+
192
+ assert_that(float('inf')).is_inf()
193
+ assert_that(float('inf') * 1).is_inf()
194
+
195
+ Returns:
196
+ AssertionBuilder: returns this instance to chain to the next assertion
197
+
198
+ Raises:
199
+ AssertionError: if val is **not** Inf
200
+ """
201
+ self._validate_number()
202
+ self._validate_real()
203
+ if not _is_infinite(self.val):
204
+ return self.error(f"Expected <{_capped(self.val)}> to be <Inf>, but was not.")
205
+ return self
206
+
207
+ def is_not_inf(self) -> Self:
208
+ """Asserts that val is real number and is *not* ``Inf`` (infinity).
209
+
210
+ Examples:
211
+ Usage:
212
+
213
+ assert_that(0).is_not_inf()
214
+ assert_that(123.4).is_not_inf()
215
+ assert_that(float('nan')).is_not_inf()
216
+
217
+ Returns:
218
+ AssertionBuilder: returns this instance to chain to the next assertion
219
+
220
+ Raises:
221
+ AssertionError: if val **is** Inf
222
+ """
223
+ self._validate_number()
224
+ self._validate_real()
225
+ if _is_infinite(self.val):
226
+ return self.error("Expected not <Inf>, but was.")
227
+ return self
228
+
229
+ def is_greater_than(self, other: object) -> Self:
230
+ """Asserts that val is numeric and is greater than other.
231
+
232
+ Args:
233
+ other: the other date, expected to be less than val
234
+
235
+ Examples:
236
+ Usage:
237
+
238
+ assert_that(1).is_greater_than(0)
239
+ assert_that(123.4).is_greater_than(111.1)
240
+
241
+ For dates, behavior is identical to [`is_after()`][assertpy.date.DateMixin.is_after]:
242
+
243
+ import datetime
244
+
245
+ today = datetime.datetime.now()
246
+ yesterday = today - datetime.timedelta(days=1)
247
+
248
+ assert_that(today).is_greater_than(yesterday)
249
+
250
+ Returns:
251
+ AssertionBuilder: returns this instance to chain to the next assertion
252
+
253
+ Raises:
254
+ AssertionError: if val is **not** greater than other
255
+
256
+ Note:
257
+ A ``NaN`` value is unordered, so it **fails** every relational assertion here
258
+ (``is_greater_than``, ``is_less_than``, ``is_between``, ``is_positive``, ...). This diverges
259
+ from assertpy 1.x, where ``NaN`` silently passes.
260
+ """
261
+ self._validate_compareable(other)
262
+ if not holds(self.val, other, "gt"): # positive form so NaN (unordered) fails instead of slipping through
263
+ return self.error(
264
+ f"Expected <{_capped_format(self.val)}> to be greater than <{_capped_format(other)}>, but was not."
265
+ f"{under(out_of_order(self.val, other, before=False, strict=True))}",
266
+ expected=other,
267
+ )
268
+ return self
269
+
270
+ def is_greater_than_or_equal_to(self, other: object) -> Self:
271
+ """Asserts that val is numeric and is greater than or equal to other.
272
+
273
+ Args:
274
+ other: the other date, expected to be less than or equal to val
275
+
276
+ Examples:
277
+ Usage:
278
+
279
+ assert_that(1).is_greater_than_or_equal_to(0)
280
+ assert_that(1).is_greater_than_or_equal_to(1)
281
+ assert_that(123.4).is_greater_than_or_equal_to(111.1)
282
+
283
+ For dates, behavior is identical to [`is_after()`][assertpy.date.DateMixin.is_after] *except* when equal:
284
+
285
+ import datetime
286
+
287
+ today = datetime.datetime.now()
288
+ yesterday = today - datetime.timedelta(days=1)
289
+
290
+ assert_that(today).is_greater_than_or_equal_to(yesterday)
291
+ assert_that(today).is_greater_than_or_equal_to(today)
292
+
293
+ Returns:
294
+ AssertionBuilder: returns this instance to chain to the next assertion
295
+
296
+ Raises:
297
+ AssertionError: if val is **not** greater than or equal to other
298
+ """
299
+ self._validate_compareable(other)
300
+ if not holds(self.val, other, "ge"): # positive form so NaN (unordered) fails instead of slipping through
301
+ return self.error(
302
+ f"Expected <{_capped_format(self.val)}> to be greater than or equal to"
303
+ f" <{_capped_format(other)}>, but was not."
304
+ f"{under(out_of_order(self.val, other, before=False, strict=False))}",
305
+ expected=other,
306
+ )
307
+ return self
308
+
309
+ def is_less_than(self, other: object) -> Self:
310
+ """Asserts that val is numeric and is less than other.
311
+
312
+ Args:
313
+ other: the other date, expected to be greater than val
314
+
315
+ Examples:
316
+ Usage:
317
+
318
+ assert_that(0).is_less_than(1)
319
+ assert_that(123.4).is_less_than(555.5)
320
+
321
+ For dates, behavior is identical to [`is_before()`][assertpy.date.DateMixin.is_before]:
322
+
323
+ import datetime
324
+
325
+ today = datetime.datetime.now()
326
+ yesterday = today - datetime.timedelta(days=1)
327
+
328
+ assert_that(yesterday).is_less_than(today)
329
+
330
+ Returns:
331
+ AssertionBuilder: returns this instance to chain to the next assertion
332
+
333
+ Raises:
334
+ AssertionError: if val is **not** less than other
335
+
336
+ Note:
337
+ A ``NaN`` value is unordered and **fails** here, diverging from assertpy 1.x where
338
+ ``NaN`` silently passes relational assertions.
339
+ """
340
+ self._validate_compareable(other)
341
+ if not holds(self.val, other, "lt"): # positive form so NaN (unordered) fails instead of slipping through
342
+ return self.error(
343
+ f"Expected <{_capped_format(self.val)}> to be less than <{_capped_format(other)}>, but was not."
344
+ f"{under(out_of_order(self.val, other, before=True, strict=True))}",
345
+ expected=other,
346
+ )
347
+ return self
348
+
349
+ def is_less_than_or_equal_to(self, other: object) -> Self:
350
+ """Asserts that val is numeric and is less than or equal to other.
351
+
352
+ Args:
353
+ other: the other date, expected to be greater than or equal to val
354
+
355
+ Examples:
356
+ Usage:
357
+
358
+ assert_that(1).is_less_than_or_equal_to(0)
359
+ assert_that(1).is_less_than_or_equal_to(1)
360
+ assert_that(123.4).is_less_than_or_equal_to(100.0)
361
+
362
+ For dates, behavior is identical to [`is_before()`][assertpy.date.DateMixin.is_before]
363
+ *except* when equal:
364
+
365
+ import datetime
366
+
367
+ today = datetime.datetime.now()
368
+ yesterday = today - datetime.timedelta(days=1)
369
+
370
+ assert_that(yesterday).is_less_than_or_equal_to(today)
371
+ assert_that(today).is_less_than_or_equal_to(today)
372
+
373
+ Returns:
374
+ AssertionBuilder: returns this instance to chain to the next assertion
375
+
376
+ Raises:
377
+ AssertionError: if val is **not** less than or equal to other
378
+ """
379
+ self._validate_compareable(other)
380
+ if not holds(self.val, other, "le"): # positive form so NaN (unordered) fails instead of slipping through
381
+ return self.error(
382
+ f"Expected <{_capped_format(self.val)}> to be less than or equal to"
383
+ f" <{_capped_format(other)}>, but was not."
384
+ f"{under(out_of_order(self.val, other, before=True, strict=False))}",
385
+ expected=other,
386
+ )
387
+ return self
388
+
389
+ def is_positive(self) -> Self:
390
+ """Asserts that val is numeric and is greater than zero.
391
+
392
+ Examples:
393
+ Usage:
394
+
395
+ assert_that(1).is_positive()
396
+ assert_that(123.4).is_positive()
397
+
398
+ Returns:
399
+ AssertionBuilder: returns this instance to chain to the next assertion
400
+
401
+ Raises:
402
+ AssertionError: if val is **not** positive
403
+
404
+ Note:
405
+ ``NaN`` is not positive, so this **fails** for ``float("nan")``. This diverges from the
406
+ assertpy 1.x, where ``NaN`` silently passes relational assertions.
407
+ """
408
+ return self.is_greater_than(0)
409
+
410
+ def is_negative(self) -> Self:
411
+ """Asserts that val is numeric and is less than zero.
412
+
413
+ Examples:
414
+ Usage:
415
+
416
+ assert_that(-1).is_negative()
417
+ assert_that(-123.4).is_negative()
418
+
419
+ Returns:
420
+ AssertionBuilder: returns this instance to chain to the next assertion
421
+
422
+ Raises:
423
+ AssertionError: if val is **not** negative
424
+
425
+ Note:
426
+ ``NaN`` is not negative, so this **fails** for ``float("nan")``. This diverges from the
427
+ assertpy 1.x, where ``NaN`` silently passes relational assertions.
428
+ """
429
+ return self.is_less_than(0)
430
+
431
+ def is_between(self, low: object, high: object) -> Self:
432
+ """Asserts that val is numeric and is between low and high.
433
+
434
+ Args:
435
+ low: the low value
436
+ high: the high value
437
+
438
+ Examples:
439
+ Usage:
440
+
441
+ assert_that(1).is_between(0, 2)
442
+ assert_that(123.4).is_between(111.1, 222.2)
443
+
444
+ For dates, works as expected:
445
+
446
+ import datetime
447
+
448
+ today = datetime.datetime.now()
449
+ middle = today - datetime.timedelta(hours=12)
450
+ yesterday = today - datetime.timedelta(days=1)
451
+
452
+ assert_that(middle).is_between(yesterday, today)
453
+
454
+ Returns:
455
+ AssertionBuilder: returns this instance to chain to the next assertion
456
+
457
+ Raises:
458
+ AssertionError: if val is **not** between low and high
459
+
460
+ Note:
461
+ ``NaN`` is not within any range, so this **fails** for ``float("nan")``. This diverges from
462
+ assertpy 1.x, where ``NaN`` silently passes relational assertions.
463
+ """
464
+ val_type = type(self.val)
465
+ self._validate_between_args(val_type, low, high)
466
+
467
+ if not self._within(low, high): # positive form so NaN (unordered) fails instead of passing
468
+ return self.error(
469
+ f"Expected <{_capped_format(self.val)}> to be between"
470
+ f" <{_capped_format(low)}> and <{_capped_format(high)}>, but was not.",
471
+ expected=(low, high),
472
+ )
473
+ return self
474
+
475
+ def is_not_between(self, low: object, high: object) -> Self:
476
+ """Asserts that val is numeric and is *not* between low and high.
477
+
478
+ Args:
479
+ low: the low value
480
+ high: the high value
481
+
482
+ Examples:
483
+ Usage:
484
+
485
+ assert_that(1).is_not_between(2, 3)
486
+ assert_that(1.1).is_not_between(2.2, 3.3)
487
+
488
+ Returns:
489
+ AssertionBuilder: returns this instance to chain to the next assertion
490
+
491
+ Raises:
492
+ AssertionError: if val **is** between low and high
493
+ """
494
+ val_type = type(self.val)
495
+ self._validate_between_args(val_type, low, high)
496
+
497
+ if self._within(low, high):
498
+ return self.error(
499
+ f"Expected <{_capped_format(self.val)}> to not be between"
500
+ f" <{_capped_format(low)}> and <{_capped_format(high)}>, but was."
501
+ )
502
+ return self
503
+
504
+ def _integer_value(self) -> int:
505
+ value = self.val
506
+ return value if type(value) is int else require_integer(value)
507
+
508
+ def is_even(self) -> Self:
509
+ """Asserts that val is an integer and is even.
510
+
511
+ Examples:
512
+ Usage:
513
+
514
+ assert_that(0).is_even()
515
+ assert_that(2).is_even()
516
+ assert_that(-4).is_even()
517
+
518
+ Returns:
519
+ AssertionBuilder: returns this instance to chain to the next assertion
520
+
521
+ Raises:
522
+ AssertionError: if val is **not** even
523
+ """
524
+ if self._integer_value() % 2 != 0:
525
+ return self.error(f"Expected <{_capped(self.val)}> to be even, but was not.")
526
+ return self
527
+
528
+ def is_odd(self) -> Self:
529
+ """Asserts that val is an integer and is odd.
530
+
531
+ Examples:
532
+ Usage:
533
+
534
+ assert_that(1).is_odd()
535
+ assert_that(3).is_odd()
536
+ assert_that(-5).is_odd()
537
+
538
+ Returns:
539
+ AssertionBuilder: returns this instance to chain to the next assertion
540
+
541
+ Raises:
542
+ AssertionError: if val is **not** odd
543
+ """
544
+ if self._integer_value() % 2 == 0:
545
+ return self.error(f"Expected <{_capped(self.val)}> to be odd, but was not.")
546
+ return self
547
+
548
+ def is_divisible_by(self, divisor: SupportsIndex) -> Self:
549
+ """Asserts that val is an integer and is divisible by divisor.
550
+
551
+ Args:
552
+ divisor: the divisor to check against (must be a non-zero integer)
553
+
554
+ Examples:
555
+ Usage:
556
+
557
+ assert_that(10).is_divisible_by(5)
558
+ assert_that(12).is_divisible_by(3)
559
+
560
+ Returns:
561
+ AssertionBuilder: returns this instance to chain to the next assertion
562
+
563
+ Raises:
564
+ AssertionError: if val is **not** divisible by divisor
565
+ """
566
+ value = self._integer_value()
567
+ modulus = divisor if type(divisor) is int else require_integer(divisor, "divisor")
568
+ if modulus == 0:
569
+ raise ValueError("given divisor arg must not be zero")
570
+ if value % modulus != 0:
571
+ return self.error(
572
+ f"Expected <{_capped(self.val)}> to be divisible by <{_capped_format(divisor)}>, but was not.",
573
+ expected=divisor,
574
+ )
575
+ return self
576
+
577
+ def is_close_to(
578
+ self,
579
+ other: SupportsFloat | SupportsIndex | datetime.datetime,
580
+ tolerance: SupportsFloat | SupportsIndex | datetime.timedelta | None = None,
581
+ *,
582
+ rel: SupportsFloat | SupportsIndex | None = None,
583
+ ) -> Self:
584
+ """Asserts that val is numeric and is close to other within tolerance.
585
+
586
+ The distance is taken three ways, the difference and the window around each side, and any of them
587
+ holding is enough, so under ``tolerance`` the answer is the same whichever operand is val. An
588
+ infinity is close only to itself, whatever the tolerance, and a NaN to nothing.
589
+
590
+ ``rel`` is a tolerance relative to the size of other, the expected value, as `pytest.approx` scales
591
+ it, and so it reads one way round: ``rel=0.02`` allows ``2`` either side of ``100``, which ``98`` is
592
+ within, while ``100`` is not within ``rel=0.02`` of ``98``. It is then measured as ``tolerance`` is.
593
+ Given beside ``tolerance``, either holding is enough, which is what keeps an expected value near
594
+ zero, where any fraction of its size is tiny, within reach.
595
+
596
+ Args:
597
+ other (object): the other value, expected to be close to val within tolerance
598
+ tolerance (object): the absolute tolerance, zero or more
599
+ rel (object): the tolerance as a fraction of the size of other, zero or more; for numbers
600
+
601
+ Examples:
602
+ Usage:
603
+
604
+ assert_that(123).is_close_to(100, 25)
605
+ assert_that(123.4).is_close_to(123, 0.5)
606
+ assert_that(101.5).is_close_to(100, rel=0.02)
607
+
608
+ For dates, works as expected:
609
+
610
+ import datetime
611
+
612
+ today = datetime.datetime.now()
613
+ yesterday = today - datetime.timedelta(days=1)
614
+
615
+ assert_that(today).is_close_to(yesterday, datetime.timedelta(hours=36))
616
+
617
+ Returns:
618
+ AssertionBuilder: returns this instance to chain to the next assertion
619
+
620
+ Raises:
621
+ AssertionError: if val is **not** close to other within tolerance
622
+ TypeError: if val, other or tolerance is not a number (or a ``datetime`` and a ``timedelta``),
623
+ or is a bool, or if a numpy duration meets a number, or if neither tolerance nor rel is given,
624
+ or rel is given for a datetime
625
+ ValueError: if tolerance or rel is negative or NaN
626
+ """
627
+ self._validate_close_to_args(self.val, other, tolerance, rel)
628
+
629
+ # read through `math.isnan`, which converts the value: the gate the capable facade keys this on
630
+ nan = not isinstance(self.val, datetime.datetime) and (_is_nan(self.val) or _is_nan(other))
631
+ if nan or not self._close(other, tolerance, rel):
632
+ if isinstance(tolerance, datetime.timedelta):
633
+ return self.error(
634
+ f"Expected <{_capped_format(self.val)}> to be close to"
635
+ f" <{_capped_format(other)}> within tolerance"
636
+ f" <{_fmt_tolerance(tolerance)}>, but was not."
637
+ f"{under(apart_in_time(self.val, other, tolerance, close=True))}",
638
+ expected=(other, tolerance),
639
+ )
640
+ else:
641
+ return self.error(
642
+ f"Expected <{_capped(self.val)}> to be close to <{_capped_format(other)}> within"
643
+ f" {_allowance(tolerance, rel)}, but was not.",
644
+ expected=(other, tolerance) if rel is None else (other, tolerance, rel),
645
+ )
646
+ return self
647
+
648
+ def is_not_close_to(
649
+ self,
650
+ other: SupportsFloat | SupportsIndex | datetime.datetime,
651
+ tolerance: SupportsFloat | SupportsIndex | datetime.timedelta | None = None,
652
+ *,
653
+ rel: SupportsFloat | SupportsIndex | None = None,
654
+ ) -> Self:
655
+ """Asserts that val is numeric and is *not* close to other within tolerance.
656
+
657
+ Measured as [`is_close_to()`][assertpy.numeric.NumericMixin.is_close_to] measures, so an infinity
658
+ is not close to anything but itself and a NaN to nothing.
659
+
660
+ Args:
661
+ other (object): the other value
662
+ tolerance (object): the absolute tolerance, zero or more
663
+ rel (object): the tolerance as a fraction of the size of other, zero or more; for numbers
664
+
665
+ Examples:
666
+ Usage:
667
+
668
+ assert_that(123).is_not_close_to(100, 22)
669
+ assert_that(123.4).is_not_close_to(123, 0.1)
670
+
671
+ Returns:
672
+ AssertionBuilder: returns this instance to chain to the next assertion
673
+
674
+ Raises:
675
+ AssertionError: if val **is** close to other within tolerance
676
+ TypeError: if val, other or tolerance is not a number (or a ``datetime`` and a ``timedelta``),
677
+ or is a bool, or if a numpy duration meets a number, or if neither tolerance nor rel is given,
678
+ or rel is given for a datetime
679
+ ValueError: if tolerance or rel is negative or NaN
680
+ """
681
+ self._validate_close_to_args(self.val, other, tolerance, rel)
682
+
683
+ if self._close(other, tolerance, rel):
684
+ if isinstance(tolerance, datetime.timedelta):
685
+ return self.error(
686
+ f"Expected <{_capped_format(self.val)}> to not be close to"
687
+ f" <{_capped_format(other)}> within tolerance"
688
+ f" <{_fmt_tolerance(tolerance)}>, but was."
689
+ f"{under(apart_in_time(self.val, other, tolerance, close=False))}",
690
+ )
691
+ else:
692
+ return self.error(
693
+ f"Expected <{_capped(self.val)}> to not be close to <{_capped_format(other)}> within"
694
+ f" {_allowance(tolerance, rel)}, but was.",
695
+ )
696
+ return self