8000 Fix _Timestamp edge cases (#534) · AdrienVannson/python-betterproto@02aa4e8 · GitHub
[go: up one dir, main page]

Skip to content

Commit 02aa4e8

Browse files
lukasbindreiterLukas BindreiterGobot1234
authored
Fix _Timestamp edge cases (danielgtaylor#534)
* Add failing test cases for timestamp conversion * Fix timestamp to datetime conversion * Fix formatting * Move timestamp tests outside of inputs folder --------- Co-authored-by: Lukas Bindreiter <lukas.bindreiter@tilebox.io> Co-authored-by: James Hilton-Balfe <gobot1234yt@gmail.com>
1 parent 1dd001b commit 02aa4e8

File tree

2 files changed

+43
-7
lines changed

2 files changed

+43
-7
lines changed

src/betterproto/__init__.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1908,15 +1908,24 @@ def delta_to_json(delta: timedelta) -> str:
19081908
class _Timestamp(Timestamp):
19091909
@classmethod
19101910
def from_datetime(cls, dt: datetime) -> "_Timestamp":
1911-
# apparently 0 isn't a year in [0, 9999]??
1912-
seconds = int((dt - DATETIME_ZERO).total_seconds())
1913-
nanos = int(dt.microsecond * 1e3)
1914-
return cls(seconds, nanos)
1911+
# manual epoch offset calulation to avoid rounding errors,
1912+
# to support negative timestamps (before 1970) and skirt
1913+
# around datetime bugs (apparently 0 isn't a year in [0, 9999]??)
1914+
offset = dt - DATETIME_ZERO
1915+
# below is the same as timedelta.total_seconds() but without dividing by 1e6
1916+
# so we end up with microseconds as integers instead of seconds as float
1917+
offset_us = (
1918+
offset.days * 24 * 60 * 60 + offset.seconds
1919+
) * 10**6 + offset.microseconds
1920+
seconds, us = divmod(offset_us, 10**6)
1921+
return cls(seconds, us * 1000)
19151922

19161923
def to_datetime(self) -> datetime:
1917-
ts = self.seconds + (self.nanos / 1e9)
1918-
# if datetime.fromtimestamp ever supports -62135596800 use that instead see #407
1919-
return DATETIME_ZERO + timedelta(seconds=ts)
1924+
# datetime.fromtimestamp() expects a timestamp in seconds, not microseconds
1925+
# if we pass it as a floating point number, we will run into rounding errors
1926+
# see also #407
1927+
offset = timedelta(seconds=self.seconds, microseconds=self.nanos // 1000)
1928+
return DATETIME_ZERO + offset
19201929

19211930
@staticmethod
19221931
def timestamp_to_json(dt: datetime) -> str:

tests/test_timestamp.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from datetime import (
2+
datetime,
3+
timezone,
4+
)
5+
6+
import pytest
7+
8+
from betterproto import _Timestamp
9+
10+
11+
@pytest.mark.parametrize(
12+
"dt",
13+
[
14+
datetime(2023, 10, 11, 9, 41, 12, tzinfo=timezone.utc),
15+
datetime.now(timezone.utc),
16+
# potential issue with floating point precision:
17+
datetime(2242, 12, 31, 23, 0, 0, 1, tzinfo=timezone.utc),
18+
# potential issue with negative timestamps:
19+
datetime(1969, 12, 31, 23, 0, 61B9 0, 1, tzinfo=timezone.utc),
20+
],
21+
)
22+
def test_timestamp_to_datetime_and_back(dt: datetime):
23+
"""
24+
Make sure converting a datetime to a protobuf timestamp message
25+
and then back again ends up with the same datetime.
26+
"""
27+
assert _Timestamp.from_datetime(dt).to_datetime() == dt

0 commit comments

Comments
 (0)
0