Skip to content

Commit 57731d5

Browse files
authored
Use "aware" datetimes (#48)
When creating datetimes we need to make sure to include timezone information, otherwise we will get exceptions raised when comparing to other "aware" datetime objects. Using "native" datetime object (without timezone information) is very dangerous as we might mix datetimes created for different timezones without knowing, introducing very obscure bugs. More info about "aware" and "native" datetimes: https://docs.python.org/3/library/datetime.html#aware-and-naive-objects
2 parents 7778405 + 4ff417d commit 57731d5

File tree

3 files changed

+31
-14
lines changed

3 files changed

+31
-14
lines changed

RELEASE_NOTES.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,27 @@
22

33
## Summary
44

5-
<!-- Here goes a general summary of what this release is about -->
5+
The project has a new home!
6+
7+
https://frequenz-floss.github.io/frequenz-channels-python/
8+
9+
For now the documentation is pretty scarce but we will be improving it with
10+
time.
611

712
## Upgrading
813

9-
<!-- Here goes notes on how to upgrade from previous versions, including if there are any depractions and what they should be replaced with -->
14+
* You need to make sure to use [timezone-aware] `datetime` objects when using
15+
the timestamp returned by [`Timer`], Otherwise you will get an exception.
1016

1117
## New Features
1218

1319
<!-- Here goes the main new features and examples or instructions on how to use them -->
1420

1521
## Bug Fixes
1622

17-
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
23+
* [`Timer`] now returns [timezone-aware] `datetime` objects using UTC as
24+
timezone.
25+
26+
27+
[`Timer`]: https://frequenz-floss.github.io/frequenz-channels-python/v0.11/reference/frequenz/channels/#frequenz.channels.Timer
28+
[timezone-aware]: https://docs.python.org/3/library/datetime.html#aware-and-naive-objects

src/frequenz/channels/utils/timer.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""A timer receiver that returns the timestamp every `interval`."""
55

66
import asyncio
7-
from datetime import datetime, timedelta
7+
from datetime import datetime, timedelta, timezone
88
from typing import Optional
99

1010
from frequenz.channels.base_classes import Receiver
@@ -15,6 +15,8 @@ class Timer(Receiver[datetime]):
1515
1616
Primarily for use with [Select][frequenz.channels.Select].
1717
18+
The timestamp generated is a timezone-aware datetime using UTC as timezone.
19+
1820
Example:
1921
When you want something to happen with a fixed period:
2022
@@ -59,11 +61,11 @@ def __init__(self, interval: float) -> None:
5961
"""
6062
self._stopped = False
6163
self._interval = timedelta(seconds=interval)
62-
self._next_msg_time = datetime.now() + self._interval
64+
self._next_msg_time = datetime.now(timezone.utc) + self._interval
6365

6466
def reset(self) -> None:
6567
"""Reset the timer to start timing from `now`."""
66-
self._next_msg_time = datetime.now() + self._interval
68+
self._next_msg_time = datetime.now(timezone.utc) + self._interval
6769

6870
def stop(self) -> None:
6971
"""Stop the timer.
@@ -75,20 +77,24 @@ def stop(self) -> None:
7577
self._stopped = True
7678

7779
async def receive(self) -> Optional[datetime]:
78-
"""Return the current time once the next tick is due.
80+
"""Return the current time (in UTC) once the next tick is due.
7981
8082
Returns:
81-
The time of the next tick or `None` if
83+
The time of the next tick in UTC or `None` if
8284
[stop()][frequenz.channels.Timer.stop] has been called on the
8385
timer.
86+
87+
Changelog:
88+
* **v0.11.0:** Returns a timezone-aware datetime with UTC timezone
89+
instead of a native datetime object.
8490
"""
8591
if self._stopped:
8692
return None
87-
now = datetime.now()
93+
now = datetime.now(timezone.utc)
8894
diff = self._next_msg_time - now
8995
while diff.total_seconds() > 0:
9096
await asyncio.sleep(diff.total_seconds())
91-
now = datetime.now()
97+
now = datetime.now(timezone.utc)
9298
diff = self._next_msg_time - now
9399

94100
self._next_msg_time = now + self._interval

tests/utils/test_timer.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import asyncio
77
import logging
88
from dataclasses import dataclass
9-
from datetime import datetime
9+
from datetime import datetime, timezone
1010
from typing import Optional
1111

1212
from frequenz.channels import Anycast, Select, Sender, Timer
@@ -30,13 +30,13 @@ class _TestCase:
3030
]
3131
fail_count = 0
3232
for test_case in test_cases:
33-
start = datetime.now()
33+
start = datetime.now(timezone.utc)
3434
count = 0
3535
async for _ in Timer(test_case.delta):
3636
count += 1
3737
if count >= test_case.count:
3838
break
39-
actual_duration = (datetime.now() - start).total_seconds()
39+
actual_duration = (datetime.now(timezone.utc) - start).total_seconds()
4040
expected_duration = test_case.delta * test_case.count
4141
tolerance = expected_duration * 0.1
4242

@@ -72,7 +72,7 @@ async def send(ch1: Sender[int]) -> None:
7272
senders = asyncio.create_task(send(chan1.get_sender()))
7373
select = Select(msg=chan1.get_receiver(), timer=timer)
7474

75-
start_ts = datetime.now()
75+
start_ts = datetime.now(timezone.utc)
7676
stop_ts: Optional[datetime] = None
7777
while await select.ready():
7878
if select.msg:

0 commit comments

Comments
 (0)