diff --git a/src/pytest_matchers/pytest_matchers/__init__.py b/src/pytest_matchers/pytest_matchers/__init__.py index 6824bae..de4b6f8 100644 --- a/src/pytest_matchers/pytest_matchers/__init__.py +++ b/src/pytest_matchers/pytest_matchers/__init__.py @@ -22,6 +22,7 @@ is_number, is_strict_dict, is_string, + is_timestamp, is_uuid, not_empty_string, one_of, diff --git a/src/pytest_matchers/pytest_matchers/main.py b/src/pytest_matchers/pytest_matchers/main.py index 4f27351..c42ac50 100644 --- a/src/pytest_matchers/pytest_matchers/main.py +++ b/src/pytest_matchers/pytest_matchers/main.py @@ -21,6 +21,7 @@ SameValue, StrictDict, String, + Timestamp, UUID, ) @@ -108,6 +109,10 @@ def is_datetime_string( return DatetimeString(expected_format, min_value=min_value, max_value=max_value) +def is_timestamp(min_value: float | Any = None, max_value: float | Any = None) -> Timestamp: + return Timestamp(min_value=min_value, max_value=max_value) + + def is_iso_8601_date(**kwargs) -> DatetimeString: return is_datetime_string("%Y-%m-%d", **kwargs) diff --git a/src/pytest_matchers/pytest_matchers/matchers/__init__.py b/src/pytest_matchers/pytest_matchers/matchers/__init__.py index b88aa4b..63ba84c 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/__init__.py +++ b/src/pytest_matchers/pytest_matchers/matchers/__init__.py @@ -26,3 +26,4 @@ from .json import JSON from .strict_dict import StrictDict from .uuid import UUID +from .timestamp import Timestamp diff --git a/src/pytest_matchers/pytest_matchers/matchers/between.py b/src/pytest_matchers/pytest_matchers/matchers/between.py index e638d6d..1c41891 100644 --- a/src/pytest_matchers/pytest_matchers/matchers/between.py +++ b/src/pytest_matchers/pytest_matchers/matchers/between.py @@ -53,7 +53,7 @@ def _matches_max(self, value: Any) -> bool: return value < self._max def _suffix_repr(self) -> str: - if self._min and self._max: + if self._min is not None and self._max is not None: if self._min_inclusive and self._max_inclusive: return f"between {self._min} and {self._max}" if not self._min_inclusive and not self._max_inclusive: @@ -70,7 +70,7 @@ def concatenated_repr(self) -> str: return self._suffix_repr() def _min_repr(self): - if not self._min: + if self._min is None: return "" return ( f"greater or equal than {self._min}" @@ -79,7 +79,7 @@ def _min_repr(self): ) def _max_repr(self): - if not self._max: + if self._max is None: return "" return ( f"lower or equal than {self._max}" if self._max_inclusive else f"lower than {self._max}" diff --git a/src/pytest_matchers/pytest_matchers/matchers/timestamp.py b/src/pytest_matchers/pytest_matchers/matchers/timestamp.py new file mode 100644 index 0000000..823ee6b --- /dev/null +++ b/src/pytest_matchers/pytest_matchers/matchers/timestamp.py @@ -0,0 +1,34 @@ +from datetime import datetime +from typing import Any + +from pytest_matchers.matchers import Matcher, Number +from pytest_matchers.matchers.between import between_matcher +from pytest_matchers.matchers.matcher_factory import matcher +from pytest_matchers.utils.matcher_utils import matches_or_none +from pytest_matchers.utils.repr_utils import concat_reprs + + +def _as_timestamp(value: float | datetime) -> float: + if isinstance(value, datetime): + return value.timestamp() + return value + + +@matcher +class Timestamp(Matcher): + def __init__(self, *, min_value: float | datetime = None, max_value: float | datetime = None): + super().__init__() + self._instance_matcher = Number() + self._limit_matcher = between_matcher( + _as_timestamp(min_value), + _as_timestamp(max_value), + None, + None, + None, + ) + + def matches(self, value: Any) -> bool: + return self._instance_matcher == value and matches_or_none(self._limit_matcher, value) + + def __repr__(self) -> str: + return concat_reprs("To be a timestamp", self._limit_matcher) diff --git a/src/tests/matchers/test_between.py b/src/tests/matchers/test_between.py index c475a43..b00c702 100644 --- a/src/tests/matchers/test_between.py +++ b/src/tests/matchers/test_between.py @@ -42,6 +42,10 @@ def test_repr(): assert repr(matcher) == "To be lower than 2" matcher = Between(1, 1) assert repr(matcher) == "To be 1" + matcher = Between(0, None) + assert repr(matcher) == "To be greater or equal than 0" + matcher = Between(None, 0) + assert repr(matcher) == "To be lower or equal than 0" def test_concatenated_repr(): @@ -59,6 +63,10 @@ def test_concatenated_repr(): assert matcher.concatenated_repr() == "lower than 2" matcher = Between(1, 1) assert matcher.concatenated_repr() == "equal to 1" + matcher = Between(0, None) + assert matcher.concatenated_repr() == "greater or equal than 0" + matcher = Between(None, 0) + assert matcher.concatenated_repr() == "lower or equal than 0" def test_matches_float_inclusive(): diff --git a/src/tests/matchers/test_is_list.py b/src/tests/matchers/test_list.py similarity index 100% rename from src/tests/matchers/test_is_list.py rename to src/tests/matchers/test_list.py diff --git a/src/tests/matchers/test_is_number.py b/src/tests/matchers/test_number.py similarity index 93% rename from src/tests/matchers/test_is_number.py rename to src/tests/matchers/test_number.py index 1228893..65104a6 100644 --- a/src/tests/matchers/test_is_number.py +++ b/src/tests/matchers/test_number.py @@ -42,6 +42,9 @@ def test_matches_number(): matcher = Number() assert matcher == 20 assert matcher == 20.0 + assert matcher == "20" + assert matcher != [] # pylint: disable=use-implicit-booleaness-not-comparison + assert matcher != None # pylint: disable=singleton-comparison assert matcher != "string" assert matcher != ["string"] @@ -50,6 +53,7 @@ def test_matches_type(): matcher = Number(int) assert matcher == 20 assert matcher != 20.0 + assert matcher != "20" assert matcher != "string" assert matcher != ["string"] diff --git a/src/tests/matchers/test_is_string.py b/src/tests/matchers/test_string.py similarity index 100% rename from src/tests/matchers/test_is_string.py rename to src/tests/matchers/test_string.py diff --git a/src/tests/matchers/test_timestamp.py b/src/tests/matchers/test_timestamp.py new file mode 100644 index 0000000..1760922 --- /dev/null +++ b/src/tests/matchers/test_timestamp.py @@ -0,0 +1,58 @@ +from datetime import datetime, timedelta + +from pytest_matchers.matchers import Timestamp + + +def test_create(): + matcher = Timestamp() + assert isinstance(matcher, Timestamp) + matcher = Timestamp(min_value=0, max_value=1) + assert isinstance(matcher, Timestamp) + matcher = Timestamp(min_value=0) + assert isinstance(matcher, Timestamp) + + +def test_repr(): + matcher = Timestamp() + assert repr(matcher) == "To be a timestamp" + matcher = Timestamp(min_value=0) + assert repr(matcher) == "To be a timestamp greater or equal than 0" + matcher = Timestamp(max_value=1) + assert repr(matcher) == "To be a timestamp lower or equal than 1" + matcher = Timestamp(min_value=0, max_value=1) + assert repr(matcher) == "To be a timestamp between 0 and 1" + + +def test_matches(): + matcher = Timestamp() + assert matcher == 0.0 + assert matcher == 1.0 + assert matcher != "string" + assert matcher == 20 + assert matcher != ["string"] + assert matcher != datetime.now() + assert matcher == datetime.now().timestamp() + + +def test_matches_with_limit(): + now_timestamp = datetime.now().timestamp() + tomorrow_timestamp = datetime.now().timestamp() + 86400 + matcher = Timestamp(min_value=now_timestamp, max_value=tomorrow_timestamp) + assert matcher == now_timestamp + assert matcher == now_timestamp + 2000 + assert matcher == tomorrow_timestamp + assert matcher == int(now_timestamp + 2000) + assert matcher != now_timestamp - 86400 + assert matcher != tomorrow_timestamp + 86400 + + +def test_matches_with_datetime_limit(): + now = datetime.now() + tomorrow = now + timedelta(days=1) + matcher = Timestamp(min_value=now, max_value=tomorrow) + assert matcher == now.timestamp() + assert matcher == now.timestamp() + 2000 + assert matcher == tomorrow.timestamp() + assert matcher == int(now.timestamp() + 2000) + assert matcher != now.timestamp() - 86400 + assert matcher != tomorrow.timestamp() + 86400 diff --git a/src/tests/test_main.py b/src/tests/test_main.py index f94c7be..2cfb984 100644 --- a/src/tests/test_main.py +++ b/src/tests/test_main.py @@ -30,6 +30,7 @@ is_number, is_strict_dict, is_string, + is_timestamp, is_uuid, not_empty_string, one_of, @@ -211,6 +212,24 @@ def test_is_datetime(): ) +def test_is_timestamp(): + date = datetime(2021, 1, 1) + assert 1612137600 == is_timestamp() + assert -1612137600 == is_timestamp() + assert date.timestamp() == is_timestamp(min_value=datetime(2021, 1, 1)) + assert date.timestamp() == is_timestamp(max_value=datetime(2021, 1, 1)) + assert date.timestamp() == is_timestamp( + min_value=datetime(2021, 1, 1), + max_value=datetime(2021, 1, 2), + ) + assert date.timestamp() != is_timestamp( + min_value=datetime(2021, 1, 2), + max_value=datetime(2021, 1, 3), + ) + assert date != is_timestamp() + assert "string" != is_timestamp() + + def test_is_datetime_string(): assert "2021-01-01" != is_datetime_string("erroneous_format") assert "2021-01-01" == is_datetime_string("%Y-%m-%d")