diff --git a/examples/all_types/define_types.py b/examples/all_types/define_types.py index 4f210b8..2470052 100644 --- a/examples/all_types/define_types.py +++ b/examples/all_types/define_types.py @@ -79,6 +79,8 @@ class MyMixedEnum(enum.Enum): "ht_datetime": ht.datetime.now(tz=dt.timezone.utc), "bt_datetime": bt.DateTime.now(tz=dt.timezone.utc), "dt_timedelta": dt.timedelta(weeks=2, days=5, minutes=12, milliseconds=75), + "ht_timedelta": ht.timedelta(days=5, seconds=25, picoseconds=88), + "bt_timedelta": bt.TimeDelta(seconds=1234.56), # supported enum and flag types "intflags": MyIntFlags.VALUE1 | MyIntFlags.VALUE4, "intenum": MyIntEnum.VALUE20, diff --git a/poetry.lock b/poetry.lock index 34f90ed..f944c50 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1201,14 +1201,14 @@ protobuf = ">=4.21" [[package]] name = "ni-protobuf-types" -version = "0.1.0.dev3" +version = "0.1.0.dev4" description = "Protobuf data types for NI gRPC APIs" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] files = [ - {file = "ni_protobuf_types-0.1.0.dev3-py3-none-any.whl", hash = "sha256:4b275f4d6f8575e607ce04482b7b1e98ace5467a7c6193ffd713e0dcd1f9eb08"}, - {file = "ni_protobuf_types-0.1.0.dev3.tar.gz", hash = "sha256:d601902dea899e034ddb7be9db7ec1e2da2a7e671ab678596763571bc0c794e8"}, + {file = "ni_protobuf_types-0.1.0.dev4-py3-none-any.whl", hash = "sha256:dceac83ca05c286e99b44898340f58b45eac91e9609444e252d89c17a7d7c1ff"}, + {file = "ni_protobuf_types-0.1.0.dev4.tar.gz", hash = "sha256:11c6a486bcb3984078e100efac93b96d5a84d70f0d8035a2303eb5cf9c0b1532"}, ] [package.dependencies] @@ -3277,4 +3277,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0,!=3.9.7" -content-hash = "bbb177180b07c7955c411b82abdf5594017e66e64b9510a5987db61d4a9bf543" +content-hash = "f10f1ed0b1fef478e655e0813863765840b0a74aceae4a67d5cf13fcef33d58a" diff --git a/pyproject.toml b/pyproject.toml index 9bcc6dd..fdeb55c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ numpy = ">=1.22" debugpy = ">=1.8.1" ni-grpc-extensions = { version = ">=0.1.0.dev1", allow-prereleases = true } ni-measurementlink-discovery-v1-client = { version = ">=0.1.0dev0", allow-prereleases = true } -ni-protobuf-types = { version = ">=0.1.0dev3", allow-prereleases = true } +ni-protobuf-types = { version = ">=0.1.0dev4", allow-prereleases = true } ni-panels-v1-proto = { version = ">=0.1.0dev1", allow-prereleases = true } [tool.poetry.group.dev.dependencies] diff --git a/src/nipanel/_convert.py b/src/nipanel/_convert.py index 9c967eb..8ea2b68 100644 --- a/src/nipanel/_convert.py +++ b/src/nipanel/_convert.py @@ -21,6 +21,7 @@ ) from nipanel.converters.protobuf_types import ( BTDateTimeConverter, + BTTimeDeltaConverter, BoolCollectionConverter, BytesCollectionConverter, DigitalWaveformConverter, @@ -30,6 +31,7 @@ DoubleSpectrumConverter, FloatCollectionConverter, HTDateTimeConverter, + HTTimeDeltaConverter, Int16AnalogWaveformConverter, Int16ComplexWaveformConverter, IntCollectionConverter, @@ -52,6 +54,7 @@ DTTimeDeltaConverter(), # Protobuf Types BTDateTimeConverter(), + BTTimeDeltaConverter(), BoolCollectionConverter(), BytesCollectionConverter(), DigitalWaveformConverter(), @@ -61,6 +64,7 @@ DoubleSpectrumConverter(), FloatCollectionConverter(), HTDateTimeConverter(), + HTTimeDeltaConverter(), Int16AnalogWaveformConverter(), Int16ComplexWaveformConverter(), IntCollectionConverter(), diff --git a/src/nipanel/_panel_value_accessor.py b/src/nipanel/_panel_value_accessor.py index 63cfc25..d4e0ec5 100644 --- a/src/nipanel/_panel_value_accessor.py +++ b/src/nipanel/_panel_value_accessor.py @@ -10,7 +10,7 @@ import nitypes.bintime as bt from ni.measurementlink.discovery.v1.client import DiscoveryClient from ni_grpc_extensions.channelpool import GrpcChannelPool -from nitypes.time import convert_datetime +from nitypes.time import convert_datetime, convert_timedelta from nipanel._panel_client import _PanelClient @@ -85,11 +85,17 @@ def get_value(self, value_id: str, default_value: _T | None = None) -> _T | obje enum_type = type(default_value) return enum_type(value) - # The grpc converter always converts PrecisionTimestamp into bt.DateTime, so - # we need to handle the case where they provide an ht.datetime default by - # converting to hightime. - if isinstance(default_value, ht.datetime) and isinstance(value, bt.DateTime): - return convert_datetime(ht.datetime, value) + # The grpc converter always converts PrecisionTimestamp into ht.datetime, so + # we need to handle the case where they provide a bt.DateTime default by + # converting to bintime. + if isinstance(default_value, bt.DateTime) and isinstance(value, ht.datetime): + return convert_datetime(bt.DateTime, value) + + # The grpc converter always converts PrecisionDuration into ht.timedelta, so + # we need to handle the case where they provide a bt.TimeDelta default by + # converting to bintime. + if isinstance(default_value, bt.TimeDelta) and isinstance(value, ht.timedelta): + return convert_timedelta(bt.TimeDelta, value) # lists are allowed to not match, since sets and tuples are converted to lists if not isinstance(value, list): diff --git a/src/nipanel/converters/protobuf_types.py b/src/nipanel/converters/protobuf_types.py index c92cd2f..d081752 100644 --- a/src/nipanel/converters/protobuf_types.py +++ b/src/nipanel/converters/protobuf_types.py @@ -10,6 +10,8 @@ import numpy as np from ni.protobuf.types import ( array_pb2, + precision_duration_pb2, + precision_duration_conversion, precision_timestamp_pb2, precision_timestamp_conversion, scalar_conversion, @@ -379,7 +381,13 @@ def to_python_value( class BTDateTimeConverter(Converter[bt.DateTime, precision_timestamp_pb2.PrecisionTimestamp]): - """A converter for bintime.DateTime types.""" + """A converter for bintime.DateTime types. + + .. note:: The nipanel package will always convert PrecisionTimestamp messages to + hightime.datetime objects using HTDateTimeConverter. To use bintime.DateTime + values in a panel, you must pass a bintime.DateTime value for the default_value + parameter of the get_value() method on the panel. + """ @property def python_type(self) -> type: @@ -391,6 +399,16 @@ def protobuf_message(self) -> Type[precision_timestamp_pb2.PrecisionTimestamp]: """The type-specific protobuf message for the Python type.""" return precision_timestamp_pb2.PrecisionTimestamp + @property + def protobuf_typename(self) -> str: + """The protobuf name for the type.""" + # Override the base class here because there can only be one converter that + # converts PrecisionTimestamp objects. Since there are two converters that convert + # to PrecisionTimestamp, we have to choose one to handle conversion from protobuf. + # For the purposes of nipanel, we'll convert PrecisionTimestamp messages to + # hightime.datetime. See HTDateTimeConverter. + return "PrecisionTimestamp_Placeholder" + def to_protobuf_message( self, python_value: bt.DateTime ) -> precision_timestamp_pb2.PrecisionTimestamp: @@ -404,34 +422,60 @@ def to_python_value( return precision_timestamp_conversion.bintime_datetime_from_protobuf(protobuf_message) -class HTDateTimeConverter(Converter[ht.datetime, precision_timestamp_pb2.PrecisionTimestamp]): - """A converter for hightime.datetime objects. +class BTTimeDeltaConverter(Converter[bt.TimeDelta, precision_duration_pb2.PrecisionDuration]): + """A converter for bintime.TimeDelta types. - .. note:: The nipanel package will always convert PrecisionTimestamp messages to - bintime.DateTime objects using BTDateTimeConverter. To use hightime.datetime - values in a panel, you must pass a hightime.datetime value for the default_value + .. note:: The nipanel package will always convert PrecisionDuration messages to + hightime.timedelta objects using HTTimeDeltaConverter. To use bintime.TimeDelta + values in a panel, you must pass a bintime.TimeDelta value for the default_value parameter of the get_value() method on the panel. """ @property def python_type(self) -> type: """The Python type that this converter handles.""" - return ht.datetime + return bt.TimeDelta @property - def protobuf_message(self) -> Type[precision_timestamp_pb2.PrecisionTimestamp]: + def protobuf_message(self) -> Type[precision_duration_pb2.PrecisionDuration]: """The type-specific protobuf message for the Python type.""" - return precision_timestamp_pb2.PrecisionTimestamp + return precision_duration_pb2.PrecisionDuration @property def protobuf_typename(self) -> str: """The protobuf name for the type.""" # Override the base class here because there can only be one converter that - # converts PrecisionTimestamp objects. Since there are two converters that convert - # to PrecisionTimestamp, we have to choose one to handle conversion from protobuf. - # For the purposes of nipanel, we'll convert PrecisionTimestamp messages to - # bintime.DateTime. See BTDateTimeConverter. - return "PrecisionTimestamp_Placeholder" + # converts PrecisionDuration objects. Since there are two converters that convert + # to PrecisionDuration, we have to choose one to handle conversion from protobuf. + # For the purposes of nipanel, we'll convert PrecisionDuration messages to + # hightime.timedelta. See HTTimeDeltaConverter. + return "PrecisionDuration_Placeholder" + + def to_protobuf_message( + self, python_value: bt.TimeDelta + ) -> precision_duration_pb2.PrecisionDuration: + """Convert the Python TimeDelta to a protobuf PrecisionDuration.""" + return precision_duration_conversion.bintime_timedelta_to_protobuf(python_value) + + def to_python_value( + self, protobuf_message: precision_duration_pb2.PrecisionDuration + ) -> bt.TimeDelta: + """Convert the protobuf PrecisionDuration to a Python TimeDelta.""" + return precision_duration_conversion.bintime_timedelta_from_protobuf(protobuf_message) + + +class HTDateTimeConverter(Converter[ht.datetime, precision_timestamp_pb2.PrecisionTimestamp]): + """A converter for hightime.datetime objects.""" + + @property + def python_type(self) -> type: + """The Python type that this converter handles.""" + return ht.datetime + + @property + def protobuf_message(self) -> Type[precision_timestamp_pb2.PrecisionTimestamp]: + """The type-specific protobuf message for the Python type.""" + return precision_timestamp_pb2.PrecisionTimestamp def to_protobuf_message( self, python_value: ht.datetime @@ -446,6 +490,32 @@ def to_python_value( return precision_timestamp_conversion.hightime_datetime_from_protobuf(protobuf_message) +class HTTimeDeltaConverter(Converter[ht.timedelta, precision_duration_pb2.PrecisionDuration]): + """A converter for hightime.timedelta objects.""" + + @property + def python_type(self) -> type: + """The Python type that this converter handles.""" + return ht.timedelta + + @property + def protobuf_message(self) -> Type[precision_duration_pb2.PrecisionDuration]: + """The type-specific protobuf message for the Python type.""" + return precision_duration_pb2.PrecisionDuration + + def to_protobuf_message( + self, python_value: ht.timedelta + ) -> precision_duration_pb2.PrecisionDuration: + """Convert the Python timedelta to a protobuf PrecisionDuration.""" + return precision_duration_conversion.hightime_timedelta_to_protobuf(python_value) + + def to_python_value( + self, protobuf_message: precision_duration_pb2.PrecisionDuration + ) -> ht.timedelta: + """Convert the protobuf PrecisionDuration to a Python timedelta.""" + return precision_duration_conversion.hightime_timedelta_from_protobuf(protobuf_message) + + class ScalarConverter(Converter[Scalar[_AnyScalarType], scalar_pb2.Scalar]): """A converter for Scalar objects.""" diff --git a/tests/unit/test_convert.py b/tests/unit/test_convert.py index 473ea7a..cfca188 100644 --- a/tests/unit/test_convert.py +++ b/tests/unit/test_convert.py @@ -10,6 +10,7 @@ from ni.protobuf.types import ( array_pb2, attribute_value_pb2, + precision_duration_pb2, precision_timestamp_pb2, scalar_pb2, vector_pb2, @@ -17,7 +18,7 @@ ) from nitypes.complex import ComplexInt32DType from nitypes.scalar import Scalar -from nitypes.time import convert_datetime +from nitypes.time import convert_datetime, convert_timedelta from nitypes.vector import Vector from nitypes.waveform import AnalogWaveform, ComplexWaveform, DigitalWaveform, Spectrum from typing_extensions import TypeAlias @@ -41,6 +42,8 @@ array_pb2.StringArray, ] +_BT_EPSILON = ht.timedelta(yoctoseconds=54210) + # ======================================================== # _get_best_matching_type() tests @@ -61,7 +64,9 @@ (dt.datetime.now(), "datetime.datetime"), (dt.timedelta(days=1), "datetime.timedelta"), (bt.DateTime.now(tz=dt.timezone.utc), "nitypes.bintime.DateTime"), + (bt.TimeDelta(seconds=1), "nitypes.bintime.TimeDelta"), (ht.datetime.now(), "hightime.datetime"), + (ht.timedelta(days=1), "hightime.timedelta"), ([False, False], "collections.abc.Collection[builtins.bool]"), ([b"mystr", b"mystr"], "collections.abc.Collection[builtins.bytes]"), ([456.2, 1.0], "collections.abc.Collection[builtins.float]"), @@ -393,6 +398,18 @@ def test___python_bintime_datetime__to_any___valid_precision_timestamp_proto() - assert unpack_dest.fractional_seconds == expected_tuple.fractional_seconds +def test___python_bintime_timedelta__to_any___valid_precision_duration_proto() -> None: + python_value = bt.TimeDelta(seconds=12.345) + + result = nipanel._convert.to_any(python_value) + unpack_dest = precision_duration_pb2.PrecisionDuration() + _assert_any_and_unpack(result, unpack_dest) + + expected_tuple = python_value.to_tuple() + assert unpack_dest.seconds == expected_tuple.whole_seconds + assert unpack_dest.fractional_seconds == expected_tuple.fractional_seconds + + def test___python_hightime_datetime__to_any___valid_precision_timestamp_proto() -> None: python_value = ht.datetime(year=2020, month=1, day=10, second=45, tzinfo=dt.timezone.utc) @@ -406,6 +423,19 @@ def test___python_hightime_datetime__to_any___valid_precision_timestamp_proto() assert unpack_dest.fractional_seconds == expected_tuple.fractional_seconds +def test___python_hightime_timedelta__to_any___valid_precision_duration_proto() -> None: + python_value = ht.timedelta(days=10, seconds=45, picoseconds=60) + + result = nipanel._convert.to_any(python_value) + unpack_dest = precision_duration_pb2.PrecisionDuration() + _assert_any_and_unpack(result, unpack_dest) + + expected_bt_timedelta = convert_timedelta(bt.TimeDelta, python_value) + expected_tuple = expected_bt_timedelta.to_tuple() + assert unpack_dest.seconds == expected_tuple.whole_seconds + assert unpack_dest.fractional_seconds == expected_tuple.fractional_seconds + + @pytest.mark.parametrize( "python_value", [ @@ -601,8 +631,9 @@ def test___double_spectrum_proto___from_any___valid_python_spectrum() -> None: assert result.frequency_increment == 10.0 -def test___precision_timestamp_proto__from_any___valid_bintime_datetime() -> None: - expected_bt_dt = bt.DateTime(year=2020, month=1, day=10, second=45, tzinfo=dt.timezone.utc) +def test___precision_timestamp_proto__from_any___valid_hightime_datetime() -> None: + expected_ht_dt = ht.datetime(year=2020, month=1, day=10, second=45, tzinfo=dt.timezone.utc) + expected_bt_dt = convert_datetime(bt.DateTime, expected_ht_dt) expected_tuple = expected_bt_dt.to_tuple() pb_value = precision_timestamp_pb2.PrecisionTimestamp( seconds=expected_tuple.whole_seconds, @@ -612,8 +643,24 @@ def test___precision_timestamp_proto__from_any___valid_bintime_datetime() -> Non result = nipanel._convert.from_any(packed_any) - assert isinstance(result, bt.DateTime) - assert result == expected_bt_dt + assert isinstance(result, ht.datetime) + assert abs(result - expected_ht_dt) <= _BT_EPSILON + + +def test___precision_duration_proto__from_any___valid_hightime_timedelta() -> None: + expected_ht_td = ht.timedelta(days=1, seconds=25, microseconds=17) + expected_bt_td = convert_timedelta(bt.TimeDelta, expected_ht_td) + expected_tuple = expected_bt_td.to_tuple() + pb_value = precision_duration_pb2.PrecisionDuration( + seconds=expected_tuple.whole_seconds, + fractional_seconds=expected_tuple.fractional_seconds, + ) + packed_any = _pack_into_any(pb_value) + + result = nipanel._convert.from_any(packed_any) + + assert isinstance(result, ht.timedelta) + assert abs(result - expected_ht_td) <= _BT_EPSILON def test___double2darray___from_any___valid_python_2dcollection() -> None: