Skip to content

Commit eb373f1

Browse files
committed
Add assert_datetime_now
1 parent c09b962 commit eb373f1

File tree

4 files changed

+124
-0
lines changed

4 files changed

+124
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ python-asserts adheres to [semantic versioning](https://semver.org/).
77
### Added
88

99
- Add support for Python 3.13 and 3.14.
10+
- Add `assert_datetime_now()`.
1011

1112
### Changed
1213

asserts/__init__.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,55 @@ def assert_has_attr(obj, attribute, msg_fmt="{msg}"):
819819
_EPSILON_SECONDS = 5
820820

821821

822+
def assert_datetime_now(
823+
actual: datetime | None,
824+
msg_fmt: str = "{msg}",
825+
*,
826+
check_utc: bool = False,
827+
) -> None:
828+
"""Fail if a timezone-aware datetime object is not close to the current time.
829+
830+
>>> assert_datetime_now(datetime.now(timezone.utc))
831+
>>> assert_datetime_now(datetime(1900, 1, 1, 12, 0, 0, tzinfo=timezone.utc))
832+
Traceback (most recent call last):
833+
...
834+
AssertionError: datetime.datetime(1900, 1, 1, 12, 0, tzinfo=datetime.timezone.utc) is not close to current date/time
835+
836+
The datetime's 'tzinfo' is asserted to be UTC if the optional parameter
837+
'check_utc' is set to True:
838+
839+
>>> import zoneinfo
840+
>>> assert_datetime_now(datetime.now(timezone.utc), check_utc=True)
841+
>>> assert_datetime_now(datetime(1900, 1, 1, 12, 0, 0, tzinfo=zoneinfo.ZoneInfo('Europe/Berlin')), check_utc=True)
842+
Traceback (most recent call last):
843+
...
844+
AssertionError: expected tzinfo of datetime.datetime(1900, 1, 1, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/Berlin')) to be UTC
845+
846+
The following msg_fmt arguments are supported:
847+
* msg - the default error message
848+
* actual - datetime object to check
849+
* now - current datetime that was tested against
850+
"""
851+
852+
now = datetime.now(timezone.utc)
853+
854+
if actual is None:
855+
msg = "None is not a valid date/time"
856+
fail(msg_fmt.format(msg=msg, actual=actual, now=now))
857+
if actual.tzinfo is None:
858+
msg = f"expected timezone-aware datetime object, got {actual!r}"
859+
fail(msg_fmt.format(msg=msg, actual=actual, now=now))
860+
if check_utc and actual.tzinfo != timezone.utc:
861+
msg = f"expected tzinfo of {actual!r} to be UTC"
862+
fail(msg_fmt.format(msg=msg, actual=actual, now=now))
863+
864+
lower_bound = now - timedelta(seconds=_EPSILON_SECONDS)
865+
upper_bound = now + timedelta(seconds=_EPSILON_SECONDS)
866+
if not lower_bound <= actual <= upper_bound:
867+
msg = f"{actual!r} is not close to current date/time"
868+
fail(msg_fmt.format(msg=msg, actual=actual, now=now))
869+
870+
822871
def assert_datetime_about_now(
823872
actual: datetime | None, msg_fmt: str = "{msg}"
824873
) -> None:

asserts/__init__.pyi

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ def assert_count_equal(
133133
def assert_has_attr(
134134
obj: object, attribute: str, msg_fmt: str = ...
135135
) -> None: ...
136+
def assert_datetime_now(
137+
actual: datetime.datetime | None,
138+
msg_fmt: str = "{msg}",
139+
*,
140+
check_utc: bool = False,
141+
) -> None: ...
136142
def assert_datetime_about_now(
137143
actual: datetime.datetime | None, msg_fmt: str = ...
138144
) -> None: ...

test_asserts.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import re
44
import sys
5+
import zoneinfo
56
from collections import OrderedDict
67
from datetime import datetime, timedelta, timezone
78
from json import JSONDecodeError
@@ -19,6 +20,7 @@
1920
assert_count_equal,
2021
assert_datetime_about_now,
2122
assert_datetime_about_now_utc,
23+
assert_datetime_now,
2224
assert_dict_equal,
2325
assert_dict_superset,
2426
assert_equal,
@@ -879,6 +881,72 @@ def test_assert_has_attr__does_not_have_attribute__custom_message(self):
879881
with _assert_raises_assertion(expected):
880882
assert_has_attr(d, "foo", msg_fmt="{msg};{obj!r};{attribute}")
881883

884+
# assert_datetime_now()
885+
886+
def test_assert_datetime_now__naive_dt(self):
887+
expected_message = "^expected timezone-aware datetime object, got "
888+
with assert_raises_regex(AssertionError, expected_message):
889+
assert_datetime_now(datetime.now())
890+
891+
def test_assert_datetime_now__close(self):
892+
assert_datetime_now(datetime.now(timezone.utc))
893+
assert_datetime_now(datetime.now(zoneinfo.ZoneInfo("Europe/Berlin")))
894+
895+
def test_assert_datetime_now__none__default_message(self):
896+
expected_message = r"^None is not a valid date/time$"
897+
with assert_raises_regex(AssertionError, expected_message):
898+
assert_datetime_now(None)
899+
900+
def test_assert_datetime_now__none__custom_message(self):
901+
dt = datetime.now().date().isoformat()
902+
expected = "None is not a valid date/time;None;{}".format(dt)
903+
with _assert_raises_assertion(expected):
904+
assert_datetime_now(
905+
None, msg_fmt="{msg};{actual!r};{now:%Y-%m-%d}"
906+
)
907+
908+
def test_assert_datetime_now__too_low(self):
909+
then = datetime.now(timezone.utc) - timedelta(minutes=1)
910+
with assert_raises(AssertionError):
911+
assert_datetime_now(then)
912+
913+
def test_assert_datetime_now__too_high(self):
914+
then = datetime.now(timezone.utc) + timedelta(minutes=1)
915+
with assert_raises(AssertionError):
916+
assert_datetime_now(then)
917+
918+
def test_assert_datetime_now__default_message(self):
919+
then = datetime(1990, 4, 13, 12, 30, 15, tzinfo=timezone.utc)
920+
expected_message = (
921+
r"^datetime.datetime\(1990, 4, 13, 12, 30, 15, tzinfo=datetime.timezone.utc\) "
922+
"is not close to current date/time$"
923+
)
924+
with assert_raises_regex(AssertionError, expected_message):
925+
assert_datetime_now(then)
926+
927+
def test_assert_datetime_now__custom_message(self):
928+
then = datetime(1990, 4, 13, 12, 30, 15, tzinfo=timezone.utc)
929+
now = datetime.now().date().isoformat()
930+
expected = (
931+
"datetime.datetime(1990, 4, 13, 12, 30, 15, tzinfo=datetime.timezone.utc) "
932+
f"is not close to current date/time;12:30;{now}"
933+
)
934+
with _assert_raises_assertion(expected):
935+
assert_datetime_now(
936+
then, msg_fmt="{msg};{actual:%H:%M};{now:%Y-%m-%d}"
937+
)
938+
939+
def test_assert_datetime_now__check_utc(self):
940+
assert_datetime_now(datetime.now(timezone.utc), check_utc=True)
941+
942+
def test_assert_datetime_now__check_utc_fail(self):
943+
expected = r"^expected tzinfo of datetime.datetime\(.*\) to be UTC"
944+
with assert_raises_regex(AssertionError, expected):
945+
assert_datetime_now(
946+
datetime.now(zoneinfo.ZoneInfo("Europe/Berlin")),
947+
check_utc=True,
948+
)
949+
882950
# assert_datetime_about_now()
883951

884952
def test_assert_datetime_about_now__aware_dt(self):

0 commit comments

Comments
 (0)