Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/_pytest/python_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pprint
import sys
import warnings
from datetime import timedelta, datetime
from decimal import Decimal
from numbers import Number

Expand Down Expand Up @@ -347,6 +348,43 @@ class ApproxDecimal(ApproxScalar):
DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6")


class ApproxDateTime(ApproxBase):
"""
Perform approximate comparisons where the expected value is a
datetime or timedelta.
"""

DEFAULT_ABSOLUTE_TOLERANCE = timedelta(seconds=1)

def __init__(self, expected, rel=None, abs=None, nan_ok=False):
if abs is None:
abs = self.DEFAULT_ABSOLUTE_TOLERANCE

if not isinstance(abs, timedelta):
raise TypeError("absolute tolerance must be a timedelta type")
if abs < timedelta(0):
raise ValueError(
"absolute tolerance can't be negative: {}".format(abs))

if rel is not None:
raise ValueError("datetime objects cannot be compared with a "
"relative tolerance")
if nan_ok:
raise ValueError("datetime objects cannot be compared to NaN")

super(ApproxDateTime, self).__init__(expected, rel=None, abs=abs,
nan_ok=False)

def __repr__(self):
if sys.version_info[0] == 2:
return "approx({!r} +- {!r})".format(self.expected, self.abs)
else:
return u"approx({!r} \u00b1 {!r})".format(self.expected, self.abs)

def __eq__(self, actual):
return abs(self.expected - actual) <= self.abs


def approx(expected, rel=None, abs=None, nan_ok=False):
"""
Assert that two numbers (or two sets of numbers) are equal to each other
Expand Down Expand Up @@ -531,6 +569,8 @@ def approx(expected, rel=None, abs=None, nan_ok=False):
and not isinstance(expected, STRING_TYPES)
):
cls = ApproxSequencelike
elif isinstance(expected, (datetime, timedelta)):
cls = ApproxDateTime
else:
raise _non_numeric_type_error(expected, at=None)

Expand Down
56 changes: 56 additions & 0 deletions testing/python/approx.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import doctest
import operator
import sys
from datetime import datetime, timezone, timedelta
from decimal import Decimal
from fractions import Fraction
from operator import eq
Expand Down Expand Up @@ -60,6 +61,17 @@ def test_repr_string(self, plus_minus):
),
)

expected = datetime(1970, 1, 1, tzinfo=timezone.utc)
tolerance = timedelta(minutes=1)
assert repr(
approx(expected, abs=tolerance)
) == 'approx({!r} {} {!r})'.format(expected, plus_minus, tolerance)
expected = timedelta(hours=1)
tolerance = timedelta(minutes=1)
assert repr(
approx(expected, abs=tolerance)
) == 'approx({!r} {} {!r})'.format(expected, plus_minus, tolerance)

@pytest.mark.parametrize(
"value, repr_string",
[
Expand Down Expand Up @@ -507,3 +519,47 @@ def __len__(self):

expected = MySizedIterable()
assert [1, 2, 3, 4] == approx(expected)

def test_datetime(self):
a_little_earlier = datetime.now()
a_little_later = datetime.now()
a_minute_later = a_little_earlier + timedelta(seconds=30)
assert a_little_later == approx(a_little_earlier)
assert a_minute_later != approx(a_little_earlier)
assert a_minute_later == approx(a_little_earlier,
abs=timedelta(minutes=1))

with pytest.raises(TypeError):
approx(a_little_earlier, abs=1)
with pytest.raises(ValueError):
approx(a_little_earlier, abs=-timedelta(seconds=1))
with pytest.raises(ValueError):
approx(a_little_earlier, rel=1)
with pytest.raises(ValueError):
approx(a_little_earlier, rel=timedelta(seconds=1))
with pytest.raises(ValueError):
approx(a_little_earlier, nan_ok=True)
with pytest.raises(TypeError):
assert timedelta(seconds=1) == approx(a_little_earlier)

def test_timedelta(self):
an_hour = timedelta(hours=1)
an_hour_and_a_bit = timedelta(hours=1, milliseconds=1)
an_hour_and_a_bit_more = timedelta(hours=1, seconds=30)
assert an_hour_and_a_bit == approx(an_hour)
assert an_hour_and_a_bit_more != approx(an_hour)
assert an_hour_and_a_bit_more == approx(an_hour,
abs=timedelta(minutes=1))

with pytest.raises(TypeError):
approx(an_hour, abs=1)
with pytest.raises(ValueError):
approx(an_hour, abs=-timedelta(seconds=1))
with pytest.raises(ValueError):
approx(an_hour, rel=1)
with pytest.raises(ValueError):
approx(an_hour, rel=timedelta(seconds=1))
with pytest.raises(ValueError):
approx(an_hour, nan_ok=True)
with pytest.raises(TypeError):
assert datetime.now() == approx(an_hour)