Skip to content

Commit 01f74c0

Browse files
committed
Add a pytest tool to compare time approximately
This tool can compare datetime or timedelta objects approximately, like pytest.approx(). It uses an absolute 1ms tolerance by default. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent a9b1040 commit 01f74c0

File tree

1 file changed

+53
-0
lines changed
  • tests/timeseries/_resampling/wall_clock_timer

1 file changed

+53
-0
lines changed

tests/timeseries/_resampling/wall_clock_timer/util.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66
from datetime import datetime, timedelta, timezone
77
from typing import assert_never
88

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+
917

1018
def to_seconds(value: datetime | timedelta | float) -> float:
1119
"""Convert a datetime, timedelta, or float to seconds."""
@@ -34,3 +42,48 @@ def wall_now() -> datetime:
3442
# isort: on
3543

3644
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

Comments
 (0)