From 77fc1b16cd6992cb4af9885bfa6988523cbb618d Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Thu, 9 Oct 2025 16:05:42 -0500 Subject: [PATCH 1/2] Add XYData conversion methods Signed-off-by: Michael Johansen --- packages/ni.protobuf.types/poetry.lock | 11 +- packages/ni.protobuf.types/pyproject.toml | 2 +- .../ni/protobuf/types/xydata_conversion.py | 39 ++++++ .../tests/unit/test_xydata_conversion.py | 129 ++++++++++++++++++ 4 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 packages/ni.protobuf.types/src/ni/protobuf/types/xydata_conversion.py create mode 100644 packages/ni.protobuf.types/tests/unit/test_xydata_conversion.py diff --git a/packages/ni.protobuf.types/poetry.lock b/packages/ni.protobuf.types/poetry.lock index da5c032b..e6e840de 100644 --- a/packages/ni.protobuf.types/poetry.lock +++ b/packages/ni.protobuf.types/poetry.lock @@ -874,14 +874,14 @@ toml = ">=0.10.1" [[package]] name = "nitypes" -version = "0.1.0.dev10" +version = "1.1.0.dev0" description = "Data types for NI Python APIs" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] files = [ - {file = "nitypes-0.1.0.dev10-py3-none-any.whl", hash = "sha256:c4f097ffdeaf05697a7752a1e2b712760b28e84a4f6f446b4b7b76b349927490"}, - {file = "nitypes-0.1.0.dev10.tar.gz", hash = "sha256:e319ebbdefca63db8e879b9f119cc4f76a086c550931dea2be0e33e9c10e9d9f"}, + {file = "nitypes-1.1.0.dev0-py3-none-any.whl", hash = "sha256:1dd0d15bb741294672ea7d61d98e951c5802eec0130d0162f4f19008dd74c0f6"}, + {file = "nitypes-1.1.0.dev0.tar.gz", hash = "sha256:a82d2f518f514a24bfb3b9c4e023db23097a786ca40155693610d09578d86f10"}, ] [package.dependencies] @@ -890,6 +890,7 @@ numpy = [ {version = ">=1.22", markers = "python_version >= \"3.9\" and python_version < \"3.13\""}, {version = ">=2.1", markers = "python_version >= \"3.13\" and python_version < \"4.0\""}, ] +typing-extensions = ">=4.13.2" [[package]] name = "nodeenv" @@ -1900,7 +1901,7 @@ version = "4.14.1" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["docs", "lint", "test"] +groups = ["main", "docs", "lint", "test"] files = [ {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, @@ -1949,4 +1950,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "1a0a03f4bbbd5fa67a4b3072c61ef82ea14fd3121c2d7ddb75223501ca9eb736" +content-hash = "4ccba63a1dd6d43cd3db273fd20137d63ea31eff38ccfa87ce0aaeabe4a67501" diff --git a/packages/ni.protobuf.types/pyproject.toml b/packages/ni.protobuf.types/pyproject.toml index 878e4714..6004a33c 100644 --- a/packages/ni.protobuf.types/pyproject.toml +++ b/packages/ni.protobuf.types/pyproject.toml @@ -39,7 +39,7 @@ requires-poetry = '>=2.1,<3.0' [tool.poetry.dependencies] protobuf = {version=">=4.21"} -nitypes = {version=">=0.1.0dev8", allow-prereleases=true} +nitypes = {version=">=1.1.0dev0", allow-prereleases=true} [tool.poetry.group.dev.dependencies] types-grpcio = ">=1.0" diff --git a/packages/ni.protobuf.types/src/ni/protobuf/types/xydata_conversion.py b/packages/ni.protobuf.types/src/ni/protobuf/types/xydata_conversion.py new file mode 100644 index 00000000..cc8c51db --- /dev/null +++ b/packages/ni.protobuf.types/src/ni/protobuf/types/xydata_conversion.py @@ -0,0 +1,39 @@ +"""Methods to convert to and from DoubleXYData protobuf messages.""" + +from __future__ import annotations + +from typing import Any + +import numpy as np +from nitypes.xy_data import XYData + +import ni.protobuf.types.xydata_pb2 as xydata_pb2 +from ni.protobuf.types.extended_property_conversion import ( + extended_properties_from_protobuf, + extended_properties_to_protobuf, +) + + +def float64_xydata_to_protobuf(value: XYData[Any], /) -> xydata_pb2.DoubleXYData: + """Convert a XYData python object to a protobuf xydata_pb2.DoubleXYData.""" + attributes = extended_properties_to_protobuf(value.extended_properties) + xydata_message = xydata_pb2.DoubleXYData( + x_data=value.x_data, + y_data=value.y_data, + attributes=attributes, + ) + return xydata_message + + +def float64_xydata_from_protobuf(message: xydata_pb2.DoubleXYData, /) -> XYData[np.float64]: + """Convert the protobuf xydata_pb2.DoubleXYData to a Python XYData.""" + xydata = XYData.from_arrays_1d( + x_array=message.x_data, + y_array=message.y_data, + dtype=np.float64, + ) + + # Transfer attributes to extended_properties + extended_properties_from_protobuf(message.attributes, xydata.extended_properties) + + return xydata diff --git a/packages/ni.protobuf.types/tests/unit/test_xydata_conversion.py b/packages/ni.protobuf.types/tests/unit/test_xydata_conversion.py new file mode 100644 index 00000000..2902e64c --- /dev/null +++ b/packages/ni.protobuf.types/tests/unit/test_xydata_conversion.py @@ -0,0 +1,129 @@ +import numpy as np +from nitypes.xy_data import XYData + +import ni.protobuf.types.xydata_pb2 as xydata_pb2 +from ni.protobuf.types.attribute_value_pb2 import AttributeValue +from ni.protobuf.types.xydata_conversion import ( + float64_xydata_from_protobuf, + float64_xydata_to_protobuf, +) + + +# ======================================================== +# XYData: Protobuf to Python +# ======================================================== +def test___doublexydata_protobuf___convert___valid_xydata_default_units() -> None: + expected_x_data = [1.0, 2.0, 3.0] + expected_y_data = [4.0, 5.0, 6.0] + protobuf_value = xydata_pb2.DoubleXYData(x_data=expected_x_data, y_data=expected_y_data) + + python_value = float64_xydata_from_protobuf(protobuf_value) + assert isinstance(python_value, XYData) + assert python_value.dtype == np.float64 + assert list(python_value.x_data) == expected_x_data + assert list(python_value.y_data) == expected_y_data + assert python_value.x_units == "" + assert python_value.y_units == "" + + +def test___doublexydata_protobuf_with_units___convert___valid_xydata() -> None: + attributes = { + "NI_UnitDescription_X": AttributeValue(string_value="Volts"), + "NI_UnitDescription_Y": AttributeValue(string_value="Seconds"), + } + expected_x_data = [1.0, 2.0, 3.0] + expected_y_data = [4.0, 5.0, 6.0] + protobuf_value = xydata_pb2.DoubleXYData( + x_data=expected_x_data, + y_data=expected_y_data, + attributes=attributes, + ) + + python_value = float64_xydata_from_protobuf(protobuf_value) + + assert isinstance(python_value, XYData) + assert python_value.dtype == np.float64 + assert list(python_value.x_data) == expected_x_data + assert list(python_value.y_data) == expected_y_data + assert python_value.x_units == "Volts" + assert python_value.y_units == "Seconds" + + +def test___doublexydata_protobuf_with_other_attrs___convert___attrs_converted() -> None: + attributes = { + "NI_UnitDescription_X": AttributeValue(string_value="Volts"), + "NI_UnitDescription_Y": AttributeValue(string_value="Seconds"), + "Non_Units_Attribute": AttributeValue(double_value=1.1), + } + protobuf_value = xydata_pb2.DoubleXYData( + x_data=[1.0], + y_data=[2.0], + attributes=attributes, + ) + + python_value = float64_xydata_from_protobuf(protobuf_value) + + assert isinstance(python_value, XYData) + assert python_value.x_units == "Volts" + assert python_value.y_units == "Seconds" + assert python_value.extended_properties.get("Non_Units_Attribute") == 1.1 + + +# ======================================================== +# XYData: Python to Protobuf +# ======================================================== +def test___float64_xydata___convert___valid_doublexydata_protobuf() -> None: + expected_x_data = [1.0, 2.0, 3.0] + expected_y_data = [4.0, 5.0, 6.0] + python_value = XYData.from_arrays_1d( + x_array=expected_x_data, + y_array=expected_y_data, + dtype=np.float64, + ) + + protobuf_value = float64_xydata_to_protobuf(python_value) + + assert isinstance(protobuf_value, xydata_pb2.DoubleXYData) + assert list(protobuf_value.x_data) == expected_x_data + assert list(protobuf_value.y_data) == expected_y_data + + +def test___int16_xydata___convert___valid_doublexydata_protobuf() -> None: + python_value = XYData.from_arrays_1d( + x_array=[1, 2, 3], + y_array=[4, 5, 6], + dtype=np.int16, + ) + + protobuf_value = float64_xydata_to_protobuf(python_value) + + assert isinstance(protobuf_value, xydata_pb2.DoubleXYData) + # Data values converted to float. Is this OK? Or should we raise an error here? + assert list(protobuf_value.x_data) == [1.0, 2.0, 3.0] + assert list(protobuf_value.y_data) == [4.0, 5.0, 6.0] + + +def test___xydata_with_extended_properties___convert___valid_doublexydata_protobuf() -> None: + expected_x_data = [1.0] + expected_y_data = [2.0] + python_value = XYData.from_arrays_1d( + x_array=expected_x_data, + y_array=expected_y_data, + dtype=np.float64, + extended_properties={ + "true": True, + "1": 1, + "1.0": 1.0, + "str": "str", + }, + ) + + protobuf_value = float64_xydata_to_protobuf(python_value) + + assert isinstance(protobuf_value, xydata_pb2.DoubleXYData) + assert list(protobuf_value.x_data) == expected_x_data + assert list(protobuf_value.y_data) == expected_y_data + assert protobuf_value.attributes["true"].bool_value is True + assert protobuf_value.attributes["1"].integer_value == 1 + assert protobuf_value.attributes["1.0"].double_value == 1.0 + assert protobuf_value.attributes["str"].string_value == "str" From 61cbfa09455b39b5e113524c64ff37b3908fd863 Mon Sep 17 00:00:00 2001 From: Michael Johansen Date: Mon, 13 Oct 2025 08:33:46 -0500 Subject: [PATCH 2/2] Narrow XYData subtype to np.float64 Signed-off-by: Michael Johansen --- .../src/ni/protobuf/types/xydata_conversion.py | 4 +--- .../tests/unit/test_xydata_conversion.py | 8 +++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/ni.protobuf.types/src/ni/protobuf/types/xydata_conversion.py b/packages/ni.protobuf.types/src/ni/protobuf/types/xydata_conversion.py index cc8c51db..15aba30c 100644 --- a/packages/ni.protobuf.types/src/ni/protobuf/types/xydata_conversion.py +++ b/packages/ni.protobuf.types/src/ni/protobuf/types/xydata_conversion.py @@ -2,8 +2,6 @@ from __future__ import annotations -from typing import Any - import numpy as np from nitypes.xy_data import XYData @@ -14,7 +12,7 @@ ) -def float64_xydata_to_protobuf(value: XYData[Any], /) -> xydata_pb2.DoubleXYData: +def float64_xydata_to_protobuf(value: XYData[np.float64], /) -> xydata_pb2.DoubleXYData: """Convert a XYData python object to a protobuf xydata_pb2.DoubleXYData.""" attributes = extended_properties_to_protobuf(value.extended_properties) xydata_message = xydata_pb2.DoubleXYData( diff --git a/packages/ni.protobuf.types/tests/unit/test_xydata_conversion.py b/packages/ni.protobuf.types/tests/unit/test_xydata_conversion.py index 2902e64c..729d3e19 100644 --- a/packages/ni.protobuf.types/tests/unit/test_xydata_conversion.py +++ b/packages/ni.protobuf.types/tests/unit/test_xydata_conversion.py @@ -88,17 +88,19 @@ def test___float64_xydata___convert___valid_doublexydata_protobuf() -> None: assert list(protobuf_value.y_data) == expected_y_data -def test___int16_xydata___convert___valid_doublexydata_protobuf() -> None: +def test___int16_xydata___convert___causes_mypy_error() -> None: python_value = XYData.from_arrays_1d( x_array=[1, 2, 3], y_array=[4, 5, 6], dtype=np.int16, ) - protobuf_value = float64_xydata_to_protobuf(python_value) + # The next line should generate a mypy error. If it doesn't, we'll get an 'unused + # ignore' mypy error. + protobuf_value = float64_xydata_to_protobuf(python_value) # type: ignore[arg-type] + # This conversion still works, so we might as well check it. Int values are converted to float. assert isinstance(protobuf_value, xydata_pb2.DoubleXYData) - # Data values converted to float. Is this OK? Or should we raise an error here? assert list(protobuf_value.x_data) == [1.0, 2.0, 3.0] assert list(protobuf_value.y_data) == [4.0, 5.0, 6.0]