Skip to content

Commit

Permalink
Add IsDate feature (#20)
Browse files Browse the repository at this point in the history
* Add IsDate feature

* add new assertion.

* fix potential bugs and remove unnecessary options

* add IsToday

* add delta option

* update docs
  • Loading branch information
cinarizasyon authored Mar 7, 2022
1 parent 17df413 commit 6e42e04
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 4 deletions.
4 changes: 3 additions & 1 deletion dirty_equals/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from ._base import AnyThing, DirtyEquals, IsInstance, IsOneOf
from ._datetime import IsDatetime, IsNow
from ._datetime import IsDate, IsDatetime, IsNow, IsToday
from ._dict import IsDict, IsIgnoreDict, IsPartialDict, IsStrictDict
from ._numeric import (
IsApprox,
Expand Down Expand Up @@ -29,6 +29,8 @@
# datetime
'IsDatetime',
'IsNow',
'IsDate',
'IsToday',
# dict
'IsDict',
'IsPartialDict',
Expand Down
111 changes: 110 additions & 1 deletion dirty_equals/_datetime.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta, timezone, tzinfo
from datetime import date, datetime, timedelta, timezone, tzinfo
from typing import Any, Optional, Union

from ._numeric import IsNumeric
Expand Down Expand Up @@ -178,3 +178,112 @@ def __init__(
)
if tz is not None:
self._repr_kwargs['tz'] = tz


class IsDate(IsNumeric[date]):
"""
Check if the value is a date, and matches the given conditions.
"""

allowed_types = date

def __init__(
self,
*,
approx: Optional[date] = None,
delta: Optional[Union[timedelta, int, float]] = None,
gt: Optional[date] = None,
lt: Optional[date] = None,
ge: Optional[date] = None,
le: Optional[date] = None,
iso_string: bool = False,
format_string: Optional[str] = None,
):

"""
Args:
approx: A value to approximately compare to.
delta: The allowable different when comparing to the value to now, if omitted 2 seconds is used,
ints and floats are assumed to represent seconds and converted to `timedelta`s.
gt: Value which the compared value should be greater than (after).
lt: Value which the compared value should be less than (before).
ge: Value which the compared value should be greater than (after) or equal to.
le: Value which the compared value should be less than (before) or equal to.
iso_string: whether to allow iso formatted strings in comparison
format_string: if provided, `format_string` is used with `datetime.strptime` to parse strings
Examples of basic usage:
```py title="IsDate"
from dirty_equals import IsDate
from datetime import date
y2k = date(2000, 1, 1)
assert date(2000, 1, 1) == IsDate(approx=y2k)
assert '2000-01-01' == IsDate(approx=y2k, iso_string=True)
assert date(2000, 1, 2) == IsDate(gt=y2k)
assert date(1999, 1, 2) != IsDate(gt=y2k)
```
"""

if delta is None:
delta = timedelta()
elif isinstance(delta, (int, float)):
delta = timedelta(seconds=delta)

super().__init__(approx=approx, gt=gt, lt=lt, ge=ge, le=le, delta=delta) # type: ignore[arg-type]

self.iso_string = iso_string
self.format_string = format_string
self._repr_kwargs.update(
iso_string=Omit if iso_string is False else iso_string,
format_string=Omit if format_string is None else format_string,
)

def prepare(self, other: Any) -> date:
if type(other) is date:
dt = other
elif isinstance(other, str):
if self.iso_string:
dt = date.fromisoformat(other)
elif self.format_string:
dt = datetime.strptime(other, self.format_string).date()
else:
raise ValueError('not a valid date string')
else:
raise ValueError(f'{type(other)} not valid as date')

return dt


class IsToday(IsDate):
"""
Check if a date is today, this is similar to `IsDate(approx=date.today())`, but slightly more powerful.
"""

def __init__(
self,
*,
iso_string: bool = False,
format_string: Optional[str] = None,
):
"""
Args:
iso_string: whether to allow iso formatted strings in comparison
format_string: if provided, `format_string` is used with `datetime.strptime` to parse strings
```py title="IsToday"
from dirty_equals import IsToday
from datetime import date, timedelta
today = date.today()
assert today == IsToday
assert today.isoformat() == IsToday(iso_string=True)
assert today.isoformat() != IsToday
assert today + timedelta(days=1) != IsToday
assert today.strftime('%Y/%m/%d') == IsToday(format_string='%Y/%m/%d')
assert today.strftime('%Y/%m/%d') != IsToday()
```
"""

super().__init__(approx=date.today(), iso_string=iso_string, format_string=format_string)
4 changes: 4 additions & 0 deletions docs/types/datetime.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ assert new_year_eve_nyc != IsDatetime(approx=new_year_naive, enforce_tz=False)
```

::: dirty_equals.IsNow

::: dirty_equals.IsDate

::: dirty_equals.IsToday
48 changes: 46 additions & 2 deletions tests/test_datetime.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from datetime import datetime, timedelta, timezone
from datetime import date, datetime, timedelta, timezone

import pytest
import pytz

from dirty_equals import IsDatetime, IsNow
from dirty_equals import IsDate, IsDatetime, IsNow, IsToday


@pytest.mark.parametrize(
Expand Down Expand Up @@ -134,3 +134,47 @@ def test_tz():
assert new_year_naive != IsDatetime(approx=new_year_eve_nyc, enforce_tz=False)
assert new_year_london == IsDatetime(approx=new_year_naive, enforce_tz=False)
assert new_year_eve_nyc != IsDatetime(approx=new_year_naive, enforce_tz=False)


@pytest.mark.parametrize(
'value,dirty,expect_match',
[
pytest.param(date(2000, 1, 1), IsDate(approx=date(2000, 1, 1)), True, id='same'),
pytest.param('2000-01-01', IsDate(approx=date(2000, 1, 1), iso_string=True), True, id='iso-string-true'),
pytest.param('2000-01-01', IsDate(approx=date(2000, 1, 1)), False, id='iso-string-different'),
pytest.param('2000-01-01T00:00', IsDate(approx=date(2000, 1, 1)), False, id='iso-string-different'),
pytest.param('broken', IsDate(approx=date(2000, 1, 1)), False, id='iso-string-wrong'),
pytest.param('28/01/87', IsDate(approx=date(1987, 1, 28), format_string='%d/%m/%y'), True, id='string-format'),
pytest.param('28/01/87', IsDate(approx=date(2000, 1, 1)), False, id='string-format-different'),
pytest.param('foobar', IsDate(approx=date(2000, 1, 1)), False, id='string-format-wrong'),
pytest.param([1, 2, 3], IsDate(approx=date(2000, 1, 1)), False, id='wrong-type'),
pytest.param(
datetime(2000, 1, 1, 10, 11, 12), IsDate(approx=date(2000, 1, 1)), False, id='wrong-type-datetime'
),
pytest.param(date(2020, 1, 1), IsDate(approx=date(2020, 1, 1)), True, id='tz-same'),
pytest.param(date(2000, 1, 1), IsDate(ge=date(2000, 1, 1)), True, id='ge'),
pytest.param(date(1999, 1, 1), IsDate(ge=date(2000, 1, 1)), False, id='ge-not'),
pytest.param(date(2000, 1, 2), IsDate(gt=date(2000, 1, 1)), True, id='gt'),
pytest.param(date(2000, 1, 1), IsDate(gt=date(2000, 1, 1)), False, id='gt-not'),
pytest.param(date(2000, 1, 1), IsDate(gt=date(2000, 1, 1), delta=10), False, id='delta-int'),
pytest.param(date(2000, 1, 1), IsDate(gt=date(2000, 1, 1), delta=10.5), False, id='delta-float'),
pytest.param(
date(2000, 1, 1), IsDate(gt=date(2000, 1, 1), delta=timedelta(seconds=10)), False, id='delta-timedelta'
),
],
)
def test_is_date(value, dirty, expect_match):
if expect_match:
assert value == dirty
else:
assert value != dirty


def test_is_today():
today = date.today()
assert today == IsToday
assert today + timedelta(days=2) != IsToday
assert today.isoformat() == IsToday(iso_string=True)
assert today.isoformat() != IsToday()
assert today.strftime('%Y/%m/%d') == IsToday(format_string='%Y/%m/%d')
assert today.strftime('%Y/%m/%d') != IsToday()

0 comments on commit 6e42e04

Please sign in to comment.