Skip to content

Commit 2b41383

Browse files
Fix dict encoding for timezone aware datetimes (#468)
Co-authored-by: James Hilton-Balfe <[email protected]>
1 parent b0b6cd2 commit 2b41383

File tree

4 files changed

+97
-0
lines changed

4 files changed

+97
-0
lines changed

src/betterproto/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,9 @@ def to_datetime(self) -> datetime:
15831583
@staticmethod
15841584
def timestamp_to_json(dt: datetime) -> str:
15851585
nanos = dt.microsecond * 1e3
1586+
if dt.tzinfo is not None:
1587+
# change timezone aware datetime objects to utc
1588+
dt = dt.astimezone(timezone.utc)
15861589
copy = dt.replace(microsecond=0, tzinfo=None)
15871590
result = copy.isoformat()
15881591
if (nanos % 1e9) == 0:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from datetime import (
2+
datetime,
3+
timedelta,
4+
timezone,
5+
)
6+
7+
import pytest
8+
9+
from tests.output_betterproto.timestamp_dict_encode import Test
10+
11+
12+
# Current World Timezone range (UTC-12 to UTC+14)
13+
MIN_UTC_OFFSET_MIN = -12 * 60
14+
MAX_UTC_OFFSET_MIN = 14 * 60
15+
16+
# Generate all timezones in range in 15 min increments
17+
timezones = [
18+
timezone(timedelta(minutes=x))
19+
for x in range(MIN_UTC_OFFSET_MIN, MAX_UTC_OFFSET_MIN + 1, 15)
20+
]
21+
22+
23+
@pytest.mark.parametrize("tz", timezones)
24+
def test_timezone_aware_datetime_dict_encode(tz: timezone):
25+
original_time = datetime.now(tz=tz)
26+
original_message = Test()
27+
original_message.ts = original_time
28+
encoded = original_message.to_dict()
29+
decoded_message = Test()
30+
decoded_message.from_dict(encoded)
31+
32+
# check that the timestamps are equal after decoding from dict
33+
assert original_message.ts.tzinfo is not None
34+
assert decoded_message.ts.tzinfo is not None
35+
assert original_message.ts == decoded_message.ts
36+
37+
38+
def test_naive_datetime_dict_encode():
39+
# make suer naive datetime objects are still treated as utc
40+
original_time = datetime.now()
41+
assert original_time.tzinfo is None
42+
original_message = Test()
43+
original_message.ts = original_time
44+
original_time_utc = original_time.replace(tzinfo=timezone.utc)
45+
encoded = original_message.to_dict()
46+
decoded_message = Test()
47+
decoded_message.from_dict(encoded)
48+
49+
# check that the timestamps are equal after decoding from dict
50+
assert decoded_message.ts.tzinfo is not None
51+
assert original_time_utc == decoded_message.ts
52+
53+
54+
@pytest.mark.parametrize("tz", timezones)
55+
def test_timezone_aware_json_serialize(tz: timezone):
56+
original_time = datetime.now(tz=tz)
57+
original_message = Test()
58+
original_message.ts = original_time
59+
json_serialized = original_message.to_json()
60+
decoded_message = Test()
61+
decoded_message.from_json(json_serialized)
62+
63+
# check that the timestamps are equal after decoding from dict
64+
assert original_message.ts.tzinfo is not None
65+
assert decoded_message.ts.tzinfo is not None
66+
assert original_message.ts == decoded_message.ts
67+
68+
69+
def test_naive_datetime_json_serialize():
70+
# make suer naive datetime objects are still treated as utc
71+
original_time = datetime.now()
72+
assert original_time.tzinfo is None
73+
original_message = Test()
74+
original_message.ts = original_time
75+
original_time_utc = original_time.replace(tzinfo=timezone.utc)
76+
json_serialized = original_message.to_json()
77+
decoded_message = Test()
78+
decoded_message.from_json(json_serialized)
79+
80+
# check that the timestamps are equal after decoding from dict
81+
assert decoded_message.ts.tzinfo is not None
82+
assert original_time_utc == decoded_message.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"ts" : "2023-03-15T22:35:51.253277Z"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
syntax = "proto3";
2+
3+
package timestamp_dict_encode;
4+
5+
import "google/protobuf/timestamp.proto";
6+
7+
message Test {
8+
google.protobuf.Timestamp ts = 1;
9+
}

0 commit comments

Comments
 (0)