diff --git a/examples/all_types/define_types.py b/examples/all_types/define_types.py index 937abc4..2e152c1 100644 --- a/examples/all_types/define_types.py +++ b/examples/all_types/define_types.py @@ -72,6 +72,7 @@ class MyMixedEnum(enum.Enum): "int": 42, "str": "sample string", "dt_datetime": dt.datetime.now(), + "dt_timedelta": dt.timedelta(weeks=2, days=5, minutes=12, milliseconds=75), # supported enum and flag types "intflags": MyIntFlags.VALUE1 | MyIntFlags.VALUE4, "intenum": MyIntEnum.VALUE20, diff --git a/src/nipanel/_convert.py b/src/nipanel/_convert.py index 6de5eff..103741a 100644 --- a/src/nipanel/_convert.py +++ b/src/nipanel/_convert.py @@ -13,6 +13,7 @@ BoolConverter, BytesConverter, DTDateTimeConverter, + DTTimeDeltaConverter, FloatConverter, IntConverter, StrConverter, @@ -39,6 +40,7 @@ IntConverter(), StrConverter(), DTDateTimeConverter(), + DTTimeDeltaConverter(), # Containers next BoolCollectionConverter(), BytesCollectionConverter(), diff --git a/src/nipanel/converters/builtin.py b/src/nipanel/converters/builtin.py index 50c9b93..2f8cd86 100644 --- a/src/nipanel/converters/builtin.py +++ b/src/nipanel/converters/builtin.py @@ -4,7 +4,7 @@ from collections.abc import Collection from typing import Type -from google.protobuf import timestamp_pb2, wrappers_pb2 +from google.protobuf import duration_pb2, timestamp_pb2, wrappers_pb2 from ni.panels.v1 import panel_types_pb2 from nipanel.converters import Converter @@ -144,6 +144,30 @@ def to_python_value(self, protobuf_message: timestamp_pb2.Timestamp) -> dt.datet return protobuf_message.ToDatetime() +class DTTimeDeltaConverter(Converter[dt.timedelta, duration_pb2.Duration]): + """A converter for datetime.timedelta types.""" + + @property + def python_typename(self) -> str: + """The Python type that this converter handles.""" + return dt.timedelta.__name__ + + @property + def protobuf_message(self) -> Type[duration_pb2.Duration]: + """The type-specific protobuf message for the Python type.""" + return duration_pb2.Duration + + def to_protobuf_message(self, python_value: dt.timedelta) -> duration_pb2.Duration: + """Convert the Python dt.timedelta to a protobuf duration_pb2.Duration.""" + dur = self.protobuf_message() + dur.FromTimedelta(python_value) + return dur + + def to_python_value(self, protobuf_message: duration_pb2.Duration) -> dt.timedelta: + """Convert the protobuf timestamp_pb2.Timestamp to a Python dt.timedelta.""" + return protobuf_message.ToTimedelta() + + class BoolCollectionConverter(Converter[Collection[bool], panel_types_pb2.BoolCollection]): """A converter for a Collection of bools.""" diff --git a/tests/unit/test_convert.py b/tests/unit/test_convert.py index ea20bef..115d33d 100644 --- a/tests/unit/test_convert.py +++ b/tests/unit/test_convert.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from google.protobuf import any_pb2, timestamp_pb2, wrappers_pb2 +from google.protobuf import any_pb2, duration_pb2, timestamp_pb2, wrappers_pb2 from google.protobuf.message import Message from ni.panels.v1 import panel_types_pb2 from ni.protobuf.types.scalar_pb2 import ScalarData @@ -55,6 +55,7 @@ (tests.types.MyStrEnum.VALUE1, "str"), (tests.types.MixinStrEnum.VALUE11, "str"), (dt.datetime.now(), "datetime"), + (dt.timedelta(days=1), "timedelta"), ([False, False], "Collection.bool"), ([b"mystr", b"mystr"], "Collection.bytes"), ([456.2, 1.0], "Collection.float"), @@ -135,6 +136,16 @@ def test___python_datetime_datetime___to_any___valid_timestamppb2_value() -> Non assert unpack_dest.ToDatetime() == expected_value +def test___python_datetime_timedelta___to_any___valid_durationpb2_value() -> None: + expected_value = dt.timedelta(days=1, seconds=2, microseconds=3) + result = nipanel._convert.to_any(expected_value) + unpack_dest = duration_pb2.Duration() + _assert_any_and_unpack(result, unpack_dest) + + assert isinstance(unpack_dest, duration_pb2.Duration) + assert unpack_dest.ToTimedelta() == expected_value + + @pytest.mark.parametrize( "proto_type, default_value, expected_value", [ @@ -199,6 +210,18 @@ def test___timestamppb2_timestamp___from_any___valid_python_value() -> None: assert result == expected_value +def test___durationpb2_timestamp___from_any___valid_python_value() -> None: + expected_value = dt.timedelta(weeks=1, hours=2, minutes=3) + pb_value = duration_pb2.Duration() + pb_value.FromTimedelta(expected_value) + packed_any = _pack_into_any(pb_value) + + result = nipanel._convert.from_any(packed_any) + + assert isinstance(result, dt.timedelta) + assert result == expected_value + + @pytest.mark.parametrize( "proto_type, expected_value", [