This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new 527994216 fix(python): return UTC-aware datetime instead of naive
datetime (#3439)
527994216 is described below
commit 52799421651f4efb699670084dd1f596fabc138b
Author: Yuta Sakuma <[email protected]>
AuthorDate: Sun Mar 1 21:22:30 2026 +0900
fix(python): return UTC-aware datetime instead of naive datetime (#3439)
## Why?
resolves issue #3430
## What does this PR do?
- Timestamp serializer returns UTC-aware datetime object.
- Added tests to verify that TimestampSerializer returns correct
UTC-aware datetime objects.
## Related issues
## Does this PR introduce any user-facing change?
- [x] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
.../idl_tests/python/idl_tests/roundtrip.py | 8 ++++--
python/pyfory/_serializer.py | 3 +--
python/pyfory/primitive.pxi | 3 +--
python/pyfory/tests/test_cross_language.py | 2 +-
python/pyfory/tests/test_serializer.py | 29 +++++++++++++++++++---
5 files changed, 34 insertions(+), 11 deletions(-)
diff --git a/integration_tests/idl_tests/python/idl_tests/roundtrip.py
b/integration_tests/idl_tests/python/idl_tests/roundtrip.py
index 142a49af3..ddc93429b 100644
--- a/integration_tests/idl_tests/python/idl_tests/roundtrip.py
+++ b/integration_tests/idl_tests/python/idl_tests/roundtrip.py
@@ -352,7 +352,9 @@ def build_optional_holder() ->
"optional_types.OptionalHolder":
string_value="optional",
bytes_value=b"\x01\x02\x03",
date_value=datetime.date(2024, 1, 2),
- timestamp_value=datetime.datetime.fromtimestamp(1704164645),
+ timestamp_value=datetime.datetime.fromtimestamp(
+ 1704164645, tz=datetime.timezone.utc
+ ),
int32_list=np.array([1, 2, 3], dtype=np.int32),
string_list=["alpha", "beta"],
int64_map={"alpha": 10, "beta": 20},
@@ -368,7 +370,9 @@ def build_any_holder() -> "any_example.AnyHolder":
bool_value=True,
string_value="hello",
date_value=datetime.date(2024, 1, 2),
- timestamp_value=datetime.datetime.fromtimestamp(1704164645),
+ timestamp_value=datetime.datetime.fromtimestamp(
+ 1704164645, tz=datetime.timezone.utc
+ ),
message_value=inner,
union_value=union_value,
list_value=["alpha", "beta"],
diff --git a/python/pyfory/_serializer.py b/python/pyfory/_serializer.py
index 8f14cece1..3d23ef4f7 100644
--- a/python/pyfory/_serializer.py
+++ b/python/pyfory/_serializer.py
@@ -284,8 +284,7 @@ class TimestampSerializer(Serializer):
seconds = buffer.read_int64()
nanos = buffer.read_uint32()
ts = seconds + nanos / 1_000_000_000
- # TODO support timezone
- return datetime.datetime.fromtimestamp(ts)
+ return datetime.datetime.fromtimestamp(ts, tz=datetime.timezone.utc)
class EnumSerializer(Serializer):
diff --git a/python/pyfory/primitive.pxi b/python/pyfory/primitive.pxi
index 279a5079a..d33923127 100644
--- a/python/pyfory/primitive.pxi
+++ b/python/pyfory/primitive.pxi
@@ -275,5 +275,4 @@ cdef class TimestampSerializer(Serializer):
cdef long long seconds = buffer.read_int64()
cdef unsigned int nanos = buffer.read_uint32()
ts = seconds + (<double>nanos) / 1000000000.0
- # TODO support timezone
- return datetime.datetime.fromtimestamp(ts)
+ return datetime.datetime.fromtimestamp(ts, tz=datetime.timezone.utc)
diff --git a/python/pyfory/tests/test_cross_language.py
b/python/pyfory/tests/test_cross_language.py
index 054c1f3f7..1e4329f7a 100644
--- a/python/pyfory/tests/test_cross_language.py
+++ b/python/pyfory/tests/test_cross_language.py
@@ -321,7 +321,7 @@ def test_cross_language_serializer(data_file_path):
assert _deserialize_and_append(fory, buffer, objects) == "str"
day = datetime.date(2021, 11, 23)
assert _deserialize_and_append(fory, buffer, objects) == day
- instant = datetime.datetime.fromtimestamp(100)
+ instant = datetime.datetime.fromtimestamp(100,
tz=datetime.timezone.utc)
assert _deserialize_and_append(fory, buffer, objects) == instant
list_ = ["a", 1, -1.0, instant, day]
assert _deserialize_and_append(fory, buffer, objects) == list_
diff --git a/python/pyfory/tests/test_serializer.py
b/python/pyfory/tests/test_serializer.py
index c37262e2f..2bc5ba675 100644
--- a/python/pyfory/tests/test_serializer.py
+++ b/python/pyfory/tests/test_serializer.py
@@ -106,7 +106,7 @@ def test_multi_chunk_simple_dict(track_ref):
@pytest.mark.parametrize("track_ref", [False, True])
def test_multi_chunk_complex_dict(track_ref):
fory = Fory(xlang=False, ref=track_ref)
- now = datetime.datetime.now()
+ now = datetime.datetime.now(datetime.timezone.utc)
day = datetime.date(2021, 11, 23)
dict0 = {"a": "a", 1: 1, -1.0: -1.0, True: True, now: now, day: day} #
noqa: F601
assert ser_de(fory, dict0) == dict0
@@ -115,7 +115,7 @@ def test_multi_chunk_complex_dict(track_ref):
@pytest.mark.parametrize("track_ref", [False, True])
def test_big_chunk_dict(track_ref):
fory = Fory(xlang=False, ref=track_ref)
- now = datetime.datetime.now()
+ now = datetime.datetime.now(datetime.timezone.utc)
day = datetime.date(2021, 11, 23)
dict0 = {}
values = ["a", 1, -1.0, True, False, now, day]
@@ -150,7 +150,7 @@ def test_basic_serializer(xlang):
assert ser_de(fory, -1.0) == -1.0
assert ser_de(fory, "str") == "str"
assert ser_de(fory, b"") == b""
- now = datetime.datetime.now()
+ now = datetime.datetime.now(datetime.timezone.utc)
assert ser_de(fory, now) == now
day = datetime.date(2021, 11, 23)
assert ser_de(fory, day) == day
@@ -164,6 +164,27 @@ def test_basic_serializer(xlang):
assert ser_de(fory, set_) == set_
[email protected]("xlang", [True, False])
+def test_timestamp_serializer(xlang):
+ """Test timestamp serialization. TimestampSerializer always returns
UTC-aware datetimes."""
+ fory = Fory(xlang=xlang, ref=False)
+
+ # Naive datetime (no timezone) — interpreted as local time on write,
always returned as UTC-aware on read.
+ naive = datetime.datetime(2026, 3, 1, 12, 30, 45, 123456)
+ result = ser_de(fory, naive)
+ assert result.tzinfo == datetime.timezone.utc
+ assert result == naive.astimezone()
+
+ aware_utc = datetime.datetime(2026, 3, 1, 12, 30, 45, 123456,
tzinfo=datetime.timezone.utc)
+ assert ser_de(fory, aware_utc) == aware_utc
+
+ # Non-UTC timezone-aware datetime (UTC+9 JST) — returned as UTC-aware with
same timestamp
+ aware_jst = datetime.datetime(2026, 3, 1, 21, 30, 45, 0,
tzinfo=datetime.timezone(datetime.timedelta(hours=9)))
+ result_jst = ser_de(fory, aware_jst)
+ assert result_jst.tzinfo == datetime.timezone.utc
+ assert result_jst.timestamp() == aware_jst.timestamp()
+
+
@pytest.mark.parametrize("xlang", [True, False])
def test_ref_tracking(xlang):
fory = Fory(xlang=xlang, ref=True)
@@ -177,7 +198,7 @@ def test_ref_tracking(xlang):
new_simple_list = ser_de(fory, simple_list)
assert new_simple_list[0] is new_simple_list
- now = datetime.datetime.now()
+ now = datetime.datetime.now(datetime.timezone.utc)
day = datetime.date(2021, 11, 23)
list_ = ["a", 1, -1.0, True, now, day]
dict1 = {f"k{i}": v for i, v in enumerate(list_)}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]