Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IsDate feature #20

Merged
merged 6 commits into from
Mar 7, 2022
Merged
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
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,
samuelcolvin marked this conversation as resolved.
Show resolved Hide resolved
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,
):

"""
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need to add IsDate to the docs, probably add it after this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only docs remained in todo. I'll do it in an available time.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this still needs adding, should be two lines.

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()