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/dataframe.py ADDED
@@ -0,0 +1,200 @@
1
+ """Optional fluent assertions for data-science containers (pandas / polars / numpy).
2
+
3
+ This is an *integration* layer in the same spirit as the Allure and Behave adapters: each library is its
4
+ own optional extra (``pip install assertpy[pandas]`` / ``[polars]`` / ``[numpy]``, or ``[data]`` for all
5
+ three), imported lazily by name, so the core stays free of runtime dependencies. Comparison
6
+ **semantics** are delegated to each library's own testing utilities (``assert_frame_equal`` /
7
+ ``assert_series_equal`` / ``assert_array_equal`` / ``assert_allclose``), so dtype, tolerance and NaN
8
+ handling are the library's, with the exceptions this layer adds: ``is_array_equal`` compares the shapes
9
+ before numpy does, and ``is_array_close_to`` defaults its tolerances to those of ``numpy.isclose`` and
10
+ takes no ``NaN`` as equal unless asked. Otherwise this layer adds the fluent entry point and routes
11
+ failures through the standard assertpy error model.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import importlib
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ from ._engine._mixin_base import _MixinBase
20
+ from ._engine._require import refuse
21
+
22
+ if TYPE_CHECKING:
23
+ from ._engine._compat import Self
24
+
25
+ __tracebackhide__ = True
26
+
27
+ _FRAME_ROOTS = ("pandas", "polars")
28
+
29
+
30
+ def _ensure_module(name: str) -> Any:
31
+ """Import optional library *name* by string, or raise a clear ImportError pointing at its extra."""
32
+ try:
33
+ return importlib.import_module(name)
34
+ except ImportError:
35
+ raise ImportError(
36
+ f"{name} is required for these assertions. Install it with: pip install assertpy[{name}]"
37
+ ) from None
38
+
39
+
40
+ def _load(root: str) -> tuple[Any, Any]:
41
+ """Return ``(library, library.testing)`` for *root* (``pandas``/``polars``/``numpy``)."""
42
+ library = _ensure_module(root)
43
+ return library, importlib.import_module(f"{root}.testing")
44
+
45
+
46
+ class DataFrameMixin(_MixinBase):
47
+ """Fluent assertions for pandas/polars frames and numpy arrays (optional ``[data]`` extra).
48
+
49
+ Each hands the comparison to the owning library's testing utilities and reports a failure through the
50
+ standard error model, so soft assertions, ``check()``, ``described_as()`` and warn mode all apply.
51
+ """
52
+
53
+ def is_frame_equal(self, expected: object, **options: Any) -> Self:
54
+ """Asserts that a pandas/polars ``DataFrame`` or ``Series`` equals *expected*.
55
+
56
+ Delegates to the owning library's own ``assert_frame_equal`` / ``assert_series_equal``, so all
57
+ comparison semantics (dtype strictness, row/column order, tolerance, categoricals, ...) are the
58
+ library's. Any keyword options are passed straight through. *expected* has to be of the same
59
+ library and kind: a polars frame against a pandas one, or a ``DataFrame`` against a ``Series``, fails
60
+ with the library's own message about the type.
61
+
62
+ Args:
63
+ expected: the expected frame/series (same library and kind as val)
64
+ **options: keyword options forwarded to the library's ``assert_frame_equal`` /
65
+ ``assert_series_equal`` (e.g. ``check_dtype=False``, ``check_exact=False``, ``rtol=1e-3``)
66
+
67
+ Examples:
68
+ Usage:
69
+
70
+ import pandas as pd
71
+
72
+ assert_that(pd.DataFrame({"a": [1, 2]})).is_frame_equal(pd.DataFrame({"a": [1, 2]}))
73
+ assert_that(actual).is_frame_equal(expected, check_dtype=False)
74
+
75
+ Returns:
76
+ AssertionBuilder: returns this instance to chain to the next assertion
77
+
78
+ Raises:
79
+ AssertionError: if the frames/series are not equal (carrying the library's own diff message)
80
+ TypeError: if val is not a pandas or polars ``DataFrame``/``Series`` (an ``Index`` is refused too)
81
+ ImportError: if the owning library is not installed
82
+ """
83
+ actual = self.val
84
+ # walk the MRO so a user subclass resolves to the owning library through its base
85
+ root = next(
86
+ (
87
+ base.__module__.split(".", 1)[0]
88
+ for base in type(actual).__mro__
89
+ if base.__module__.split(".", 1)[0] in _FRAME_ROOTS
90
+ ),
91
+ type(actual).__module__.split(".", 1)[0],
92
+ )
93
+ if root not in _FRAME_ROOTS:
94
+ refuse(actual, "a pandas or polars DataFrame/Series")
95
+ library, testing = _load(root)
96
+ class_name = type(actual).__name__
97
+ # isinstance handles real subclasses, the name check handles duck-typed frames a test may inject
98
+ if isinstance(actual, library.Series) or class_name == "Series":
99
+ assert_equal, label = testing.assert_series_equal, "Series"
100
+ elif isinstance(actual, library.DataFrame) or class_name == "DataFrame":
101
+ assert_equal, label = testing.assert_frame_equal, "DataFrame"
102
+ else:
103
+ # a pandas/polars object that is neither a DataFrame nor a Series (Index, Categorical, ...)
104
+ refuse(actual, "a pandas or polars DataFrame/Series")
105
+ try:
106
+ assert_equal(actual, expected, **options)
107
+ except AssertionError as exc:
108
+ return self.error(
109
+ f"Expected the {label} to equal the expected one, but they differ:\n{exc}",
110
+ suppress_context=True,
111
+ expected=expected,
112
+ )
113
+ return self
114
+
115
+ def is_array_equal(self, expected: object, **options: Any) -> Self:
116
+ """Asserts that val equals *expected* element-wise, via numpy's ``assert_array_equal``.
117
+
118
+ Works on any array-likes numpy can coerce (``ndarray``, nested lists, ...). The shapes are compared
119
+ before numpy is asked, so a scalar is never broadcast over an array. Then every element must match
120
+ exactly, a ``NaN`` equal to a ``NaN`` in the same position, as numpy has it.
121
+
122
+ Args:
123
+ expected: the expected array-like
124
+ **options: keyword options forwarded to numpy's ``assert_array_equal``
125
+ (e.g. ``strict=True``, which compares the dtypes too, or ``err_msg="..."``)
126
+
127
+ Examples:
128
+ Usage:
129
+
130
+ import numpy as np
131
+
132
+ assert_that(np.array([1, 2, 3])).is_array_equal(np.array([1, 2, 3]))
133
+ assert_that(np.array([1, 2, 3])).is_array_equal(np.array([1, 2, 3]), strict=True)
134
+ assert_that(np.array([1, 2, 3])).is_array_equal([1, 2, 3])
135
+
136
+ Returns:
137
+ AssertionBuilder: returns this instance to chain to the next assertion
138
+
139
+ Raises:
140
+ AssertionError: if the shapes differ, or the arrays are not equal (carrying numpy's own diff message)
141
+ ImportError: if numpy is not installed
142
+ """
143
+ numpy, testing = _load("numpy")
144
+ # shape first: left to numpy a scalar broadcasts, so an empty array "equals" 5, against the shape
145
+ # the docstring promises. numpy's own `strict` is about dtype and is passed through untouched
146
+ actual_shape, expected_shape = numpy.shape(self.val), numpy.shape(expected)
147
+ if actual_shape != expected_shape:
148
+ return self.error(
149
+ f"Expected an array of shape <{expected_shape}>, but was of shape <{actual_shape}>.",
150
+ expected=expected,
151
+ )
152
+ try:
153
+ testing.assert_array_equal(self.val, expected, **options)
154
+ except AssertionError as exc:
155
+ return self.error(
156
+ f"Expected the arrays to be equal, but they differ:\n{exc}", suppress_context=True, expected=expected
157
+ )
158
+ return self
159
+
160
+ def is_array_close_to(
161
+ self, expected: object, *, rtol: float = 1e-05, atol: float = 1e-08, equal_nan: bool = False, **options: Any
162
+ ) -> Self:
163
+ """Asserts that val is element-wise close to *expected*, via numpy's ``assert_allclose``.
164
+
165
+ The float-tolerant counterpart to [`is_array_equal()`][assertpy.dataframe.DataFrameMixin.is_array_equal],
166
+ for comparing computed arrays. The shapes are numpy's to compare, so a scalar broadcasts over an
167
+ array unless ``strict=True`` is passed, which numpy 2 accepts. ``equal_nan`` defaults to ``False``,
168
+ where numpy's own default is ``True``, so a ``NaN`` fails unless asked for.
169
+
170
+ Args:
171
+ expected: the expected array-like
172
+ rtol: relative tolerance (``1e-05``, as ``numpy.isclose`` has it, where ``assert_allclose`` has ``1e-07``)
173
+ atol: absolute tolerance (``1e-08``, as ``numpy.isclose`` has it, where ``assert_allclose`` has ``0``)
174
+ equal_nan: whether ``NaN`` in the same position compares equal (``False`` here, ``True`` in numpy)
175
+ **options: further keyword options forwarded to numpy's ``assert_allclose``
176
+ (e.g. ``err_msg="..."``, ``strict=True``)
177
+
178
+ Examples:
179
+ Usage:
180
+
181
+ import numpy as np
182
+
183
+ assert_that(np.array([1.0, 2.0])).is_array_close_to(np.array([1.0, 2.0000001]))
184
+ assert_that(np.array([np.nan])).is_array_close_to(np.array([np.nan]), equal_nan=True)
185
+
186
+ Returns:
187
+ AssertionBuilder: returns this instance to chain to the next assertion
188
+
189
+ Raises:
190
+ AssertionError: if the arrays are not close (carrying numpy's own diff message)
191
+ ImportError: if numpy is not installed
192
+ """
193
+ _, testing = _load("numpy")
194
+ try:
195
+ testing.assert_allclose(self.val, expected, rtol=rtol, atol=atol, equal_nan=equal_nan, **options)
196
+ except AssertionError as exc:
197
+ return self.error(
198
+ f"Expected the arrays to be close, but they differ:\n{exc}", suppress_context=True, expected=expected
199
+ )
200
+ return self
assertpy/date.py ADDED
@@ -0,0 +1,327 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ from typing import TYPE_CHECKING, cast
5
+
6
+ from ._engine._mixin_base import _MixinBase
7
+ from ._engine._require import argument, require_type
8
+ from ._hints import out_of_order, under
9
+ from .errors import _capped, _capped_format
10
+
11
+ if TYPE_CHECKING:
12
+ from ._engine._compat import Self
13
+
14
+ __tracebackhide__ = True
15
+
16
+
17
+ def _require_datetime(value: object, label: str) -> None:
18
+ """Raise ``TypeError`` unless *value* is exactly a `datetime.datetime`."""
19
+ require_type(value, datetime.datetime, "a datetime", subject=label)
20
+
21
+
22
+ def _read_in_the_zone_of(value: datetime.datetime, reference: datetime.datetime) -> datetime.datetime:
23
+ """*value* as the same instant reads on *reference*'s clock, so wall-clock fields compare like with like.
24
+
25
+ The guide says making both sides aware is what makes these comparisons well defined. Compared field
26
+ by field they were not: two instants five hours apart in different zones read equal to the second.
27
+ Both sides are aware or both naive by then, and a naive pair has no zone to be read in. One wall time
28
+ of a zone's repeated hour is two moments an hour apart, so the caller asks whether the two are one reading
29
+ too (`_one_reading`) where the value was read on another clock. Two on one zone object are left to their
30
+ wall clocks, as `==` is.
31
+ """
32
+ return value.astimezone(reference.tzinfo) if reference.tzinfo is not None else value
33
+
34
+
35
+ def _one_reading(value: datetime.datetime, instant: datetime.datetime, other: datetime.datetime) -> bool:
36
+ """Whether two that read alike on one clock are the same reading of it, where a clock reads a time twice.
37
+
38
+ The offsets tell. Equal ones are one reading. Two readings of one time have the later moment at the
39
+ smaller offset, since the clock went back between them, and the later moment at the larger one is a clock
40
+ that went forward inside the unit, which reads it once. Asked of the `fold` instead, a clock that goes
41
+ back by less than the unit read two visits to one minute as one.
42
+ """
43
+ # both are aware here: a naive pair has no zone to be read in and is never asked
44
+ held, read = cast("datetime.timedelta", value.utcoffset()), cast("datetime.timedelta", instant.utcoffset())
45
+ return held == read or (held < read) is (value < other)
46
+
47
+
48
+ def _written_to(form: str, value: datetime.datetime, instant: datetime.datetime) -> tuple[str, str]:
49
+ """The two in *form*, with their offsets where they read alike: a wall time of a repeated hour is two moments."""
50
+ first, second = value.strftime(form), instant.strftime(form)
51
+ if first != second:
52
+ return first, second
53
+ return first + _offset_of(value), second + _offset_of(instant)
54
+
55
+
56
+ def _offset_of(value: datetime.datetime) -> str:
57
+ written = value.strftime("%z")
58
+ return f"{written[:3]}:{written[3:]}"
59
+
60
+
61
+ def _require_comparable_datetimes(first: datetime.datetime, second: datetime.datetime) -> None:
62
+ """Reject a naive-vs-aware pair with a clear message.
63
+
64
+ The relational assertions would otherwise leak the raw ``TypeError`` from ``<``/``>``. The
65
+ ``ignoring_*`` ones compare wall-clock fields, which silently answers a different question: two
66
+ instants hours apart read equal. Both operands are already validated as `datetime.datetime`.
67
+ """
68
+ if (first.utcoffset() is None) != (second.utcoffset() is None):
69
+ raise TypeError(
70
+ "cannot compare a timezone-naive datetime with a timezone-aware one. Make both aware or both naive first"
71
+ )
72
+
73
+
74
+ class DateMixin(_MixinBase):
75
+ """Date and time assertions mixin."""
76
+
77
+ def is_before(self, other: datetime.datetime) -> Self:
78
+ """Asserts that val is a date and is before other date.
79
+
80
+ Args:
81
+ other (object): the other date, expected to be after val
82
+
83
+ Examples:
84
+ Usage:
85
+
86
+ import datetime
87
+
88
+ today = datetime.datetime.now()
89
+ yesterday = today - datetime.timedelta(days=1)
90
+
91
+ assert_that(yesterday).is_before(today)
92
+
93
+ Returns:
94
+ AssertionBuilder: returns this instance to chain to the next assertion
95
+
96
+ Raises:
97
+ AssertionError: if val is **not** before the given date
98
+
99
+ See Also:
100
+ [`is_less_than()`][assertpy.numeric.NumericMixin.is_less_than] - numeric assertion, but
101
+ also works with datetime
102
+ [`is_less_than_or_equal_to()`][assertpy.numeric.NumericMixin.is_less_than_or_equal_to] -
103
+ also works with datetime
104
+ """
105
+ _require_datetime(self.val, "val")
106
+ _require_datetime(other, argument("other"))
107
+ _require_comparable_datetimes(self.val, other)
108
+ if self.val >= other:
109
+ return self.error(
110
+ f"Expected <{_capped(self.val)}> to be before <{_capped_format(other)}>, but was not."
111
+ f"{under(out_of_order(self.val, other, before=True, strict=True))}",
112
+ expected=other,
113
+ )
114
+ return self
115
+
116
+ def is_after(self, other: datetime.datetime) -> Self:
117
+ """Asserts that val is a date and is after other date.
118
+
119
+ Args:
120
+ other (object): the other date, expected to be before val
121
+
122
+ Examples:
123
+ Usage:
124
+
125
+ import datetime
126
+
127
+ today = datetime.datetime.now()
128
+ yesterday = today - datetime.timedelta(days=1)
129
+
130
+ assert_that(today).is_after(yesterday)
131
+
132
+ Returns:
133
+ AssertionBuilder: returns this instance to chain to the next assertion
134
+
135
+ Raises:
136
+ AssertionError: if val is **not** after the given date
137
+
138
+ See Also:
139
+ [`is_greater_than()`][assertpy.numeric.NumericMixin.is_greater_than] - numeric assertion,
140
+ but also works with datetime
141
+ [`is_greater_than_or_equal_to()`][assertpy.numeric.NumericMixin.is_greater_than_or_equal_to] -
142
+ also works with datetime
143
+ """
144
+ _require_datetime(self.val, "val")
145
+ _require_datetime(other, argument("other"))
146
+ _require_comparable_datetimes(self.val, other)
147
+ if self.val <= other:
148
+ return self.error(
149
+ f"Expected <{_capped(self.val)}> to be after <{_capped_format(other)}>, but was not."
150
+ f"{under(out_of_order(self.val, other, before=False, strict=True))}",
151
+ expected=other,
152
+ )
153
+ return self
154
+
155
+ def is_before_or_equal_to(self, other: datetime.datetime) -> Self:
156
+ """Asserts that val is a date and is before or equal to other date.
157
+
158
+ Args:
159
+ other (object): the other date, expected to be after or equal to val
160
+
161
+ Examples:
162
+ Usage:
163
+
164
+ import datetime
165
+
166
+ today = datetime.datetime.now()
167
+ yesterday = today - datetime.timedelta(days=1)
168
+
169
+ assert_that(yesterday).is_before_or_equal_to(today)
170
+ assert_that(today).is_before_or_equal_to(today)
171
+
172
+ Returns:
173
+ AssertionBuilder: returns this instance to chain to the next assertion
174
+
175
+ Raises:
176
+ AssertionError: if val is **not** before or equal to the given date
177
+ """
178
+ _require_datetime(self.val, "val")
179
+ _require_datetime(other, argument("other"))
180
+ _require_comparable_datetimes(self.val, other)
181
+ if self.val > other:
182
+ return self.error(
183
+ f"Expected <{_capped(self.val)}> to be before or equal to <{_capped_format(other)}>, but was not."
184
+ f"{under(out_of_order(self.val, other, before=True, strict=False))}",
185
+ expected=other,
186
+ )
187
+ return self
188
+
189
+ def is_after_or_equal_to(self, other: datetime.datetime) -> Self:
190
+ """Asserts that val is a date and is after or equal to other date.
191
+
192
+ Args:
193
+ other (object): the other date, expected to be before or equal to val
194
+
195
+ Examples:
196
+ Usage:
197
+
198
+ import datetime
199
+
200
+ today = datetime.datetime.now()
201
+ yesterday = today - datetime.timedelta(days=1)
202
+
203
+ assert_that(today).is_after_or_equal_to(yesterday)
204
+ assert_that(today).is_after_or_equal_to(today)
205
+
206
+ Returns:
207
+ AssertionBuilder: returns this instance to chain to the next assertion
208
+
209
+ Raises:
210
+ AssertionError: if val is **not** after or equal to the given date
211
+ """
212
+ _require_datetime(self.val, "val")
213
+ _require_datetime(other, argument("other"))
214
+ _require_comparable_datetimes(self.val, other)
215
+ if self.val < other:
216
+ return self.error(
217
+ f"Expected <{_capped(self.val)}> to be after or equal to <{_capped_format(other)}>, but was not."
218
+ f"{under(out_of_order(self.val, other, before=False, strict=False))}",
219
+ expected=other,
220
+ )
221
+ return self
222
+
223
+ def is_equal_to_ignoring_milliseconds(self, other: datetime.datetime) -> Self:
224
+ """Asserts that val is a date and is equal to other date to the second.
225
+
226
+ Args:
227
+ other (object): the other date, expected to be equal to the second
228
+
229
+ Examples:
230
+ Usage:
231
+
232
+ import datetime
233
+
234
+ d1 = datetime.datetime(2020, 1, 2, 3, 4, 5, 6) # 2020-01-02 03:04:05.000006
235
+ d2 = datetime.datetime(2020, 1, 2, 3, 4, 5, 777777) # 2020-01-02 03:04:05.777777
236
+
237
+ assert_that(d1).is_equal_to_ignoring_milliseconds(d2)
238
+
239
+ Returns:
240
+ AssertionBuilder: returns this instance to chain to the next assertion
241
+
242
+ Raises:
243
+ AssertionError: if val is **not** equal to the given date to the second
244
+ """
245
+ _require_datetime(self.val, "val")
246
+ _require_datetime(other, argument("other"))
247
+ _require_comparable_datetimes(self.val, other)
248
+ instant = _read_in_the_zone_of(other, self.val)
249
+ if (
250
+ self.val.date() == instant.date()
251
+ and self.val.hour == instant.hour
252
+ and self.val.minute == instant.minute
253
+ and self.val.second == instant.second
254
+ and (instant is other or _one_reading(self.val, instant, other))
255
+ ):
256
+ return self
257
+ read, wanted = _written_to("%Y-%m-%d %H:%M:%S", self.val, instant)
258
+ return self.error(f"Expected <{read}> to be equal to <{wanted}>, but was not.", expected=other)
259
+
260
+ def is_equal_to_ignoring_seconds(self, other: datetime.datetime) -> Self:
261
+ """Asserts that val is a date and is equal to other date to the minute.
262
+
263
+ Args:
264
+ other (object): the other date, expected to be equal to the minute
265
+
266
+ Examples:
267
+ Usage:
268
+
269
+ import datetime
270
+
271
+ d1 = datetime.datetime(2020, 1, 2, 3, 4, 5) # 2020-01-02 03:04:05
272
+ d2 = datetime.datetime(2020, 1, 2, 3, 4, 55) # 2020-01-02 03:04:55
273
+
274
+ assert_that(d1).is_equal_to_ignoring_seconds(d2)
275
+
276
+ Returns:
277
+ AssertionBuilder: returns this instance to chain to the next assertion
278
+
279
+ Raises:
280
+ AssertionError: if val is **not** equal to the given date to the minute
281
+ """
282
+ _require_datetime(self.val, "val")
283
+ _require_datetime(other, argument("other"))
284
+ _require_comparable_datetimes(self.val, other)
285
+ instant = _read_in_the_zone_of(other, self.val)
286
+ if (
287
+ self.val.date() == instant.date()
288
+ and self.val.hour == instant.hour
289
+ and self.val.minute == instant.minute
290
+ and (instant is other or _one_reading(self.val, instant, other))
291
+ ):
292
+ return self
293
+ read, wanted = _written_to("%Y-%m-%d %H:%M", self.val, instant)
294
+ return self.error(f"Expected <{read}> to be equal to <{wanted}>, but was not.", expected=other)
295
+
296
+ def is_equal_to_ignoring_time(self, other: datetime.datetime) -> Self:
297
+ """Asserts that val is a date and is equal to other date ignoring time.
298
+
299
+ Args:
300
+ other (object): the other date, expected to be equal ignoring time
301
+
302
+ Examples:
303
+ Usage:
304
+
305
+ import datetime
306
+
307
+ d1 = datetime.datetime(2020, 1, 2, 3, 4, 5) # 2020-01-02 03:04:05
308
+ d2 = datetime.datetime(2020, 1, 2, 13, 44, 55) # 2020-01-02 13:44:55
309
+
310
+ assert_that(d1).is_equal_to_ignoring_time(d2)
311
+
312
+ Returns:
313
+ AssertionBuilder: returns this instance to chain to the next assertion
314
+
315
+ Raises:
316
+ AssertionError: if val is **not** equal to the given date ignoring time
317
+ """
318
+ _require_datetime(self.val, "val")
319
+ _require_datetime(other, argument("other"))
320
+ _require_comparable_datetimes(self.val, other)
321
+ instant = _read_in_the_zone_of(other, self.val)
322
+ if self.val.date() == instant.date():
323
+ return self
324
+ return self.error(
325
+ f"Expected <{self.val.strftime('%Y-%m-%d')}> to be equal to <{instant.strftime('%Y-%m-%d')}>, but was not.",
326
+ expected=other,
327
+ )