Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/all_types/define_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
4 changes: 4 additions & 0 deletions src/nipanel/_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
)
from nipanel.converters.protobuf_types import (
BTDateTimeConverter,
BTTimeDeltaConverter,
BoolCollectionConverter,
BytesCollectionConverter,
DigitalWaveformConverter,
Expand All @@ -30,6 +31,7 @@
DoubleSpectrumConverter,
FloatCollectionConverter,
HTDateTimeConverter,
HTTimeDeltaConverter,
Int16AnalogWaveformConverter,
Int16ComplexWaveformConverter,
IntCollectionConverter,
Expand All @@ -52,6 +54,7 @@
DTTimeDeltaConverter(),
# Protobuf Types
BTDateTimeConverter(),
BTTimeDeltaConverter(),
BoolCollectionConverter(),
BytesCollectionConverter(),
DigitalWaveformConverter(),
Expand All @@ -61,6 +64,7 @@
DoubleSpectrumConverter(),
FloatCollectionConverter(),
HTDateTimeConverter(),
HTTimeDeltaConverter(),
Int16AnalogWaveformConverter(),
Int16ComplexWaveformConverter(),
IntCollectionConverter(),
Expand Down
18 changes: 12 additions & 6 deletions src/nipanel/_panel_value_accessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down
98 changes: 84 additions & 14 deletions src/nipanel/converters/protobuf_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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."""

Expand Down
57 changes: 52 additions & 5 deletions tests/unit/test_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
from ni.protobuf.types import (
array_pb2,
attribute_value_pb2,
precision_duration_pb2,
precision_timestamp_pb2,
scalar_pb2,
vector_pb2,
waveform_pb2,
)
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
Expand All @@ -41,6 +42,8 @@
array_pb2.StringArray,
]

_BT_EPSILON = ht.timedelta(yoctoseconds=54210)


# ========================================================
# _get_best_matching_type() tests
Expand All @@ -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]"),
Expand Down Expand Up @@ -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)

Expand All @@ -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",
[
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand Down
Loading