|
6 | 6 | from datetime import datetime, timedelta, timezone |
7 | 7 | from typing import assert_never |
8 | 8 |
|
| 9 | +# This is not great, we are depending on an internal pytest API, but it is |
| 10 | +# the most convenient way to provide a custom approx() comparison for datetime |
| 11 | +# and timedelta. |
| 12 | +# Other alternatives proven to be even more complex and hacky. |
| 13 | +# It also looks like we are not the only ones doing this, see: |
| 14 | +# https://github.com/pytest-dev/pytest/issues/8395 |
| 15 | +from _pytest.python_api import ApproxBase |
| 16 | + |
9 | 17 |
|
10 | 18 | def to_seconds(value: datetime | timedelta | float) -> float: |
11 | 19 | """Convert a datetime, timedelta, or float to seconds.""" |
@@ -34,3 +42,48 @@ def wall_now() -> datetime: |
34 | 42 | # isort: on |
35 | 43 |
|
36 | 44 | return mock_datetime.now(timezone.utc) |
| 45 | + |
| 46 | + |
| 47 | +# Pylint complains about abstract-method because _yield_comparisons is not implemented |
| 48 | +# but it is used only in the default __eq__ method, which we are re-defining, so we can |
| 49 | +# ignore it. |
| 50 | +class approx_time(ApproxBase): # pylint: disable=invalid-name, abstract-method |
| 51 | + """Perform approximate comparisons for datetime or timedelta objects. |
| 52 | +
|
| 53 | + Inherits from `ApproxBase` to provide a rich comparison output in pytest. |
| 54 | + """ |
| 55 | + |
| 56 | + expected: datetime | timedelta |
| 57 | + abs: timedelta |
| 58 | + |
| 59 | + def __init__( |
| 60 | + self, |
| 61 | + expected: datetime | timedelta, |
| 62 | + *, |
| 63 | + abs: timedelta = timedelta(milliseconds=1), # pylint: disable=redefined-builtin |
| 64 | + ) -> None: |
| 65 | + """Initialize this instance.""" |
| 66 | + if abs < timedelta(): |
| 67 | + raise ValueError( |
| 68 | + f"absolute tolerance must be a non-negative timedelta, not {abs}" |
| 69 | + ) |
| 70 | + super().__init__(expected, abs=abs) |
| 71 | + |
| 72 | + def __repr__(self) -> str: |
| 73 | + """Return a string representation of this instance.""" |
| 74 | + return f"{self.expected} ± {self.abs}" |
| 75 | + |
| 76 | + def __eq__(self, actual: object) -> bool: |
| 77 | + """Compare this instance with another object.""" |
| 78 | + # We need to split the cases for datetime and timedelta for type checking |
| 79 | + # reasons. |
| 80 | + diff: timedelta |
| 81 | + match (self.expected, actual): |
| 82 | + case (datetime(), datetime()): |
| 83 | + diff = self.expected - actual |
| 84 | + case (timedelta(), timedelta()): |
| 85 | + diff = self.expected - actual |
| 86 | + case _: |
| 87 | + return NotImplemented |
| 88 | + |
| 89 | + return abs(diff) <= self.abs |
0 commit comments