From e7a460ec7ca1d9bd9cd9b3c1c7e12186d8e549f8 Mon Sep 17 00:00:00 2001 From: Brian Maissy Date: Sun, 10 Feb 2019 00:54:49 +0200 Subject: [PATCH] preliminay implementation of comparing datetimes and timedeltas with approx() --- src/_pytest/python_api.py | 40 ++++++++++++++++++++++++++++ testing/python/approx.py | 56 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 1b643d4301f..e23d32fd530 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -4,6 +4,7 @@ import pprint import sys import warnings +from datetime import timedelta, datetime from decimal import Decimal from numbers import Number @@ -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 @@ -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) diff --git a/testing/python/approx.py b/testing/python/approx.py index 26e6a4ab209..bd7fea5384d 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -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 @@ -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", [ @@ -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)