From b8909eca84a2126b4186d12078a4f4bf11a26f40 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Thu, 27 Apr 2023 19:32:54 +0100 Subject: [PATCH] numeric exact (#64) --- dirty_equals/_numeric.py | 21 ++++++++++++++++++--- tests/test_numeric.py | 18 +++++++++++++++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/dirty_equals/_numeric.py b/dirty_equals/_numeric.py index 15d2437..02a0282 100644 --- a/dirty_equals/_numeric.py +++ b/dirty_equals/_numeric.py @@ -41,6 +41,7 @@ class IsNumeric(DirtyEquals[N]): def __init__( self, *, + exactly: Optional[N] = None, approx: Optional[N] = None, delta: Optional[N] = None, gt: Optional[N] = None, @@ -50,6 +51,8 @@ def __init__( ): """ Args: + exactly: A value to exactly compare to - useful when you want to make sure a value is an `int` or `float`, + while also checking its value. approx: A value to approximately compare to. delta: The allowable different when comparing to the value to `approx`, if omitted `value / 100` is used except for datetimes where 2 seconds is used. @@ -74,16 +77,22 @@ def __init__( assert d == IsNumeric(approx=datetime(2020, 1, 1, 12, 0, 1)) ``` """ + self.exactly: Optional[N] = exactly + if self.exactly is not None and (gt, lt, ge, le) != (None, None, None, None): + raise TypeError('"exactly" cannot be combined with "gt", "lt", "ge", or "le"') + if self.exactly is not None and approx is not None: + raise TypeError('"exactly" cannot be combined with "approx"') self.approx: Optional[N] = approx - self.delta: Optional[N] = delta if self.approx is not None and (gt, lt, ge, le) != (None, None, None, None): raise TypeError('"approx" cannot be combined with "gt", "lt", "ge", or "le"') + self.delta: Optional[N] = delta self.gt: Optional[N] = gt self.lt: Optional[N] = lt self.ge: Optional[N] = ge self.le: Optional[N] = le - self.has_bounds_checks = not all(f is None for f in (approx, delta, gt, lt, ge, le)) + self.has_bounds_checks = not all(f is None for f in (exactly, approx, delta, gt, lt, ge, le)) kwargs = { + 'exactly': Omit if exactly is None else exactly, 'approx': Omit if approx is None else approx, 'delta': Omit if delta is None else delta, 'gt': Omit if gt is None else gt, @@ -110,7 +119,9 @@ def equals(self, other: Any) -> bool: return True def bounds_checks(self, other: N) -> bool: - if self.approx is not None: + if self.exactly is not None: + return self.exactly == other + elif self.approx is not None: if self.delta is None: if isinstance(other, date): delta: Any = timedelta(seconds=2) @@ -278,6 +289,8 @@ class IsInt(IsNumeric[int]): assert 1.0 != IsInt assert 'foobar' != IsInt assert True != IsInt + assert 1 == IsInt(exactly=1) + assert -2 != IsInt(exactly=1) ``` """ @@ -341,6 +354,8 @@ class IsFloat(IsNumeric[float]): assert 1.0 == IsFloat assert 1 != IsFloat + assert 1.0 == IsFloat(exactly=1.0) + assert 1.001 != IsFloat(exactly=1.0) ``` """ diff --git a/tests/test_numeric.py b/tests/test_numeric.py index dd64381..e246410 100644 --- a/tests/test_numeric.py +++ b/tests/test_numeric.py @@ -24,9 +24,11 @@ [ (1, IsInt), (1, IsInt()), + (1, IsInt(exactly=1)), (1, IsPositiveInt), (-1, IsNegativeInt), (-1.0, IsFloat), + (-1.0, IsFloat(exactly=-1.0)), (1.0, IsPositiveFloat), (-1.0, IsNegativeFloat), (1, IsPositive), @@ -72,6 +74,7 @@ def test_dirty_equals(other, dirty): [ (1.0, IsInt), (1.2, IsInt), + (1, IsInt(exactly=2)), (True, IsInt), (False, IsInt), (1.0, IsInt()), @@ -80,6 +83,8 @@ def test_dirty_equals(other, dirty): (1, IsNegativeInt), (0, IsNegativeInt), (1, IsFloat), + (1, IsFloat(exactly=1.0)), + (1.1234, IsFloat(exactly=1.0)), (-1.0, IsPositiveFloat), (0.0, IsPositiveFloat), (1.0, IsNegativeFloat), @@ -105,16 +110,27 @@ def test_dirty_equals(other, dirty): (-float('-inf'), IsFloatInfNeg), (-float('-inf'), IsFloatInfNeg), ], + ids=repr, ) def test_dirty_not_equals(other, dirty): assert other != dirty -def test_invalid(): +def test_invalid_approx_gt(): with pytest.raises(TypeError, match='"approx" cannot be combined with "gt", "lt", "ge", or "le"'): IsInt(approx=1, gt=1) +def test_invalid_exactly_approx(): + with pytest.raises(TypeError, match='"exactly" cannot be combined with "approx"'): + IsInt(exactly=1, approx=1) + + +def test_invalid_exactly_gt(): + with pytest.raises(TypeError, match='"exactly" cannot be combined with "gt", "lt", "ge", or "le"'): + IsInt(exactly=1, gt=1) + + def test_not_int(): d = IsInt() with pytest.raises(AssertionError):