From 9b2d0e0a974d19388c59bb39365f0cf3629f0575 Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Mon, 16 Oct 2023 03:35:32 +0100 Subject: [PATCH] Fix timestamp parsing (#415) Fixes #407 --- pyproject.toml | 8 +++++-- src/betterproto/__init__.py | 6 ++++-- .../google_impl_behavior_equivalence.proto | 7 ++++++- .../test_google_impl_behavior_equivalence.py | 21 +++++++++++++++++++ 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 225c023e3..383b6233c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,9 +62,13 @@ help = "Run tests" cmd = "mypy src --ignore-missing-imports" help = "Check types with mypy" +[tool.poe.tasks] +_black = "black . --exclude tests/output_ --target-version py310" +_isort = "isort . --extend-skip-glob 'tests/output_*/**/*'" + [tool.poe.tasks.format] -cmd = "black . --exclude tests/output_ --target-version py310" -help = "Apply black formatting to source code" +sequence = ["_black", "_isort"] +help = "Apply black and isort formatting to source code" [tool.poe.tasks.docs] cmd = "sphinx-build docs docs/build" diff --git a/src/betterproto/__init__.py b/src/betterproto/__init__.py index 2ff3df151..8f42fa449 100644 --- a/src/betterproto/__init__.py +++ b/src/betterproto/__init__.py @@ -1885,13 +1885,15 @@ def delta_to_json(delta: timedelta) -> str: class _Timestamp(Timestamp): @classmethod def from_datetime(cls, dt: datetime) -> "_Timestamp": - seconds = int(dt.timestamp()) + # apparently 0 isn't a year in [0, 9999]?? + seconds = int((dt - DATETIME_ZERO).total_seconds()) nanos = int(dt.microsecond * 1e3) return cls(seconds, nanos) def to_datetime(self) -> datetime: ts = self.seconds + (self.nanos / 1e9) - return datetime.fromtimestamp(ts, tz=timezone.utc) + # if datetime.fromtimestamp ever supports -62135596800 use that instead see #407 + return DATETIME_ZERO + timedelta(seconds=ts) @staticmethod def timestamp_to_json(dt: datetime) -> str: diff --git a/tests/inputs/google_impl_behavior_equivalence/google_impl_behavior_equivalence.proto b/tests/inputs/google_impl_behavior_equivalence/google_impl_behavior_equivalence.proto index 3e9bea99b..66ef8a645 100644 --- a/tests/inputs/google_impl_behavior_equivalence/google_impl_behavior_equivalence.proto +++ b/tests/inputs/google_impl_behavior_equivalence/google_impl_behavior_equivalence.proto @@ -1,5 +1,6 @@ syntax = "proto3"; +import "google/protobuf/timestamp.proto"; package google_impl_behavior_equivalence; message Foo { int64 bar = 1; } @@ -12,6 +13,10 @@ message Test { } } +message Spam { + google.protobuf.Timestamp ts = 1; +} + message Request { Empty foo = 1; } -message Empty {} \ No newline at end of file +message Empty {} diff --git a/tests/inputs/google_impl_behavior_equivalence/test_google_impl_behavior_equivalence.py b/tests/inputs/google_impl_behavior_equivalence/test_google_impl_behavior_equivalence.py index dd2a9f53e..6d2991bdd 100644 --- a/tests/inputs/google_impl_behavior_equivalence/test_google_impl_behavior_equivalence.py +++ b/tests/inputs/google_impl_behavior_equivalence/test_google_impl_behavior_equivalence.py @@ -1,17 +1,25 @@ +from datetime import ( + datetime, + timezone, +) + import pytest from google.protobuf import json_format +from google.protobuf.timestamp_pb2 import Timestamp import betterproto from tests.output_betterproto.google_impl_behavior_equivalence import ( Empty, Foo, Request, + Spam, Test, ) from tests.output_reference.google_impl_behavior_equivalence.google_impl_behavior_equivalence_pb2 import ( Empty as ReferenceEmpty, Foo as ReferenceFoo, Request as ReferenceRequest, + Spam as ReferenceSpam, Test as ReferenceTest, ) @@ -59,6 +67,19 @@ def test_bytes_are_the_same_for_oneof(): assert isinstance(message_reference2.foo, ReferenceFoo) +@pytest.mark.parametrize("dt", (datetime.min.replace(tzinfo=timezone.utc),)) +def test_datetime_clamping(dt): # see #407 + ts = Timestamp() + ts.FromDatetime(dt) + assert bytes(Spam(dt)) == ReferenceSpam(ts=ts).SerializeToString() + message_bytes = bytes(Spam(dt)) + + assert ( + Spam().parse(message_bytes).ts.timestamp() + == ReferenceSpam.FromString(message_bytes).ts.seconds + ) + + def test_empty_message_field(): message = Request() reference_message = ReferenceRequest()