Skip to content

Commit 8386932

Browse files
mjohanse-emrMichael Johansen
andauthored
Move Scalar conversion logic into ni-apis-python. (#22)
* Move Scalar conversion logic into ni-apis-python. * Check the result of WhichOneOf for None. Signed-off-by: Michael Johansen <[email protected]> --------- Signed-off-by: Michael Johansen <[email protected]> Co-authored-by: Michael Johansen <[email protected]>
1 parent 678b427 commit 8386932

File tree

4 files changed

+280
-1
lines changed

4 files changed

+280
-1
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""Methods to convert to and from scalar protobuf messages."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Mapping
6+
from typing import Union
7+
8+
from nitypes.scalar import Scalar
9+
from nitypes.waveform import ExtendedPropertyDictionary, ExtendedPropertyValue
10+
from typing_extensions import TypeAlias
11+
12+
import ni.protobuf.types.scalar_pb2 as scalar_pb2
13+
from ni.protobuf.types.attribute_value_pb2 import AttributeValue
14+
15+
_AnyScalarType: TypeAlias = Union[bool, int, float, str]
16+
_SCALAR_TYPE_TO_PB_ATTR_MAP = {
17+
bool: "bool_value",
18+
int: "int32_value",
19+
float: "double_value",
20+
str: "string_value",
21+
}
22+
23+
24+
def scalar_to_protobuf(value: Scalar[_AnyScalarType], /) -> scalar_pb2.Scalar:
25+
"""Convert a Scalar python object to a protobuf scalar_pb2.Scalar."""
26+
attributes = _extended_properties_to_attributes(value.extended_properties)
27+
message = scalar_pb2.Scalar(attributes=attributes)
28+
29+
# Convert the scalar value
30+
value_attr = _SCALAR_TYPE_TO_PB_ATTR_MAP.get(type(value.value), None)
31+
if not value_attr:
32+
raise TypeError(f"Unexpected type for python_value.value: {type(value.value)}")
33+
setattr(message, value_attr, value.value)
34+
35+
return message
36+
37+
38+
def scalar_from_protobuf(message: scalar_pb2.Scalar, /) -> Scalar[_AnyScalarType]:
39+
"""Convert the protobuf scalar_pb2.Scalar to a Python Scalar."""
40+
# Convert the scalar value.
41+
pb_type = message.WhichOneof("value")
42+
if pb_type is None:
43+
raise ValueError("Could not determine the data type of 'value'.")
44+
45+
if pb_type not in _SCALAR_TYPE_TO_PB_ATTR_MAP.values():
46+
raise ValueError(f"Unexpected value for protobuf_value.WhichOneOf: {pb_type}")
47+
value = getattr(message, pb_type)
48+
49+
# Create with blank units. Units from the protobuf message will be populated
50+
# when attributes are converted to an ExtendedPropertyDictionary.
51+
scalar = Scalar(value, "")
52+
53+
# Transfer attributes to extended_properties
54+
for key, value in message.attributes.items():
55+
attr_type = value.WhichOneof("attribute")
56+
if attr_type is None:
57+
raise ValueError("Could not determine the data type of 'attribute'.")
58+
scalar.extended_properties[key] = getattr(value, attr_type)
59+
60+
return scalar
61+
62+
63+
def _extended_properties_to_attributes(
64+
extended_properties: ExtendedPropertyDictionary,
65+
) -> Mapping[str, AttributeValue]:
66+
return {key: _value_to_attribute(value) for key, value in extended_properties.items()}
67+
68+
69+
def _value_to_attribute(value: ExtendedPropertyValue) -> AttributeValue:
70+
attr_value = AttributeValue()
71+
if isinstance(value, bool):
72+
attr_value.bool_value = value
73+
elif isinstance(value, int):
74+
attr_value.integer_value = value
75+
elif isinstance(value, float):
76+
attr_value.double_value = value
77+
elif isinstance(value, str):
78+
attr_value.string_value = value
79+
else:
80+
raise TypeError(f"Unexpected type for extended property value {type(value)}")
81+
82+
return attr_value

packages/ni.protobuf.types/src/ni/protobuf/types/waveform_conversion.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ def float64_analog_waveform_from_protobuf(
7878
extended_properties = {}
7979
for key, value in message.attributes.items():
8080
attr_type = value.WhichOneof("attribute")
81-
extended_properties[key] = getattr(value, str(attr_type))
81+
if attr_type is None:
82+
raise ValueError("Could not determine the datatype of 'attribute'.")
83+
extended_properties[key] = getattr(value, attr_type)
8284

8385
data_array = np.array(message.y_data)
8486
return AnalogWaveform(

packages/ni.protobuf.types/tests/unit/test_ni_protobuf_types.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""Tests for the ni.protobuf.types package."""
22

33
from ni.protobuf.types.array_pb2 import Double2DArray, String2DArray
4+
from ni.protobuf.types.attribute_value_pb2 import AttributeValue
45
from ni.protobuf.types.precision_timestamp_pb2 import PrecisionTimestamp
6+
from ni.protobuf.types.scalar_pb2 import Scalar
57
from ni.protobuf.types.waveform_pb2 import (
68
DoubleAnalogWaveform,
79
DoubleComplexWaveform,
@@ -18,6 +20,10 @@
1820
"attr1": WaveformAttributeValue(integer_value=1),
1921
"attr2": WaveformAttributeValue(string_value="two"),
2022
}
23+
EXPECTED_SCALAR_ATTRIBUTES = {
24+
"attr1": AttributeValue(integer_value=1),
25+
"attr2": AttributeValue(string_value="two"),
26+
}
2127

2228

2329
def test___valid_inputs___createdouble2darray___message_created() -> None:
@@ -114,3 +120,31 @@ def test___valid_inputs___create_doublexydata___message_created() -> None:
114120

115121
assert list(test_wfm.x_data) == [1.0, 2.0, 3.0]
116122
assert list(test_wfm.y_data) == [4.0, 5.0, 6.0]
123+
124+
125+
def test___valid_inputs___create_double_scalar___message_created() -> None:
126+
test_scalar = Scalar(attributes=EXPECTED_SCALAR_ATTRIBUTES, double_value=1.0)
127+
128+
assert test_scalar.double_value == 1.0
129+
assert test_scalar.attributes == EXPECTED_SCALAR_ATTRIBUTES
130+
131+
132+
def test___valid_inputs___create_int_scalar___message_created() -> None:
133+
test_scalar = Scalar(attributes=EXPECTED_SCALAR_ATTRIBUTES, int32_value=1)
134+
135+
assert test_scalar.int32_value == 1
136+
assert test_scalar.attributes == EXPECTED_SCALAR_ATTRIBUTES
137+
138+
139+
def test___valid_inputs___create_bool_scalar___message_created() -> None:
140+
test_scalar = Scalar(attributes=EXPECTED_SCALAR_ATTRIBUTES, bool_value=True)
141+
142+
assert test_scalar.bool_value is True
143+
assert test_scalar.attributes == EXPECTED_SCALAR_ATTRIBUTES
144+
145+
146+
def test___valid_inputs___create_string_scalar___message_created() -> None:
147+
test_scalar = Scalar(attributes=EXPECTED_SCALAR_ATTRIBUTES, string_value="one")
148+
149+
assert test_scalar.string_value == "one"
150+
assert test_scalar.attributes == EXPECTED_SCALAR_ATTRIBUTES
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import pytest
2+
from nitypes.scalar import Scalar
3+
4+
import ni.protobuf.types.scalar_pb2 as scalar_pb2
5+
from ni.protobuf.types.attribute_value_pb2 import AttributeValue
6+
from ni.protobuf.types.scalar_conversion import scalar_from_protobuf, scalar_to_protobuf
7+
8+
9+
# ========================================================
10+
# Scalar: Protobuf to Python
11+
# ========================================================
12+
def test___bool_scalar_protobuf___convert___valid_bool_scalar() -> None:
13+
attributes = {"NI_UnitDescription": AttributeValue(string_value="Volts")}
14+
protobuf_value = scalar_pb2.Scalar(attributes=attributes)
15+
protobuf_value.bool_value = True
16+
17+
python_value = scalar_from_protobuf(protobuf_value)
18+
19+
assert isinstance(python_value.value, bool)
20+
assert python_value.value is True
21+
assert python_value.units == "Volts"
22+
23+
24+
def test___int32_scalar_protobuf___convert___valid_int_scalar() -> None:
25+
attributes = {"NI_UnitDescription": AttributeValue(string_value="Volts")}
26+
protobuf_value = scalar_pb2.Scalar(attributes=attributes)
27+
protobuf_value.int32_value = 10
28+
29+
python_value = scalar_from_protobuf(protobuf_value)
30+
31+
assert isinstance(python_value.value, int)
32+
assert python_value.value == 10
33+
assert python_value.units == "Volts"
34+
35+
36+
def test___double_scalar_protobuf___convert___valid_float_scalar() -> None:
37+
attributes = {"NI_UnitDescription": AttributeValue(string_value="Volts")}
38+
protobuf_value = scalar_pb2.Scalar(attributes=attributes)
39+
protobuf_value.double_value = 20.0
40+
41+
python_value = scalar_from_protobuf(protobuf_value)
42+
43+
assert isinstance(python_value.value, float)
44+
assert python_value.value == 20.0
45+
assert python_value.units == "Volts"
46+
47+
48+
def test___string_scalar_protobuf___convert___valid_str_scalar() -> None:
49+
attributes = {"NI_UnitDescription": AttributeValue(string_value="Volts")}
50+
protobuf_value = scalar_pb2.Scalar(attributes=attributes)
51+
protobuf_value.string_value = "value"
52+
53+
python_value = scalar_from_protobuf(protobuf_value)
54+
55+
assert isinstance(python_value.value, str)
56+
assert python_value.value == "value"
57+
assert python_value.units == "Volts"
58+
59+
60+
def test___scalar_protobuf_value_unset___convert___throws_value_error() -> None:
61+
attributes = {"NI_UnitDescription": AttributeValue(string_value="Volts")}
62+
protobuf_value = scalar_pb2.Scalar(attributes=attributes)
63+
64+
with pytest.raises(ValueError) as exc:
65+
_ = scalar_from_protobuf(protobuf_value)
66+
67+
assert exc.value.args[0].startswith("Could not determine the data type of 'value'.")
68+
69+
70+
def test___scalar_protobuf_units_unset___convert___python_units_blank() -> None:
71+
protobuf_value = scalar_pb2.Scalar()
72+
protobuf_value.bool_value = True
73+
74+
python_value = scalar_from_protobuf(protobuf_value)
75+
76+
assert isinstance(python_value.value, bool)
77+
assert python_value.value is True
78+
assert python_value.units == ""
79+
80+
81+
def test___non_units_attributes___to_python___attributes_converted() -> None:
82+
attributes = {
83+
"NI_ChannelName": AttributeValue(string_value="Dev1/ai0"),
84+
"NI_UnitDescription": AttributeValue(string_value="Volts"),
85+
}
86+
protobuf_value = scalar_pb2.Scalar(attributes=attributes)
87+
protobuf_value.string_value = "value"
88+
89+
python_value = scalar_from_protobuf(protobuf_value)
90+
channel_name = python_value.extended_properties["NI_ChannelName"]
91+
92+
assert isinstance(python_value.value, str)
93+
assert python_value.value == "value"
94+
assert python_value.units == "Volts"
95+
assert isinstance(channel_name, str)
96+
assert channel_name == "Dev1/ai0"
97+
98+
99+
# ========================================================
100+
# Scalar: Python to Protobuf
101+
# ========================================================
102+
def test___bool_scalar___convert___valid_bool_scalar_protobuf() -> None:
103+
python_value = Scalar(True, "Volts")
104+
105+
protobuf_value = scalar_to_protobuf(python_value)
106+
107+
assert protobuf_value.WhichOneof("value") == "bool_value"
108+
assert protobuf_value.bool_value is True
109+
assert protobuf_value.attributes["NI_UnitDescription"].string_value == "Volts"
110+
111+
112+
def test___int_scalar___convert___valid_int32_scalar_protobuf() -> None:
113+
python_value = Scalar(10, "Volts")
114+
115+
protobuf_value = scalar_to_protobuf(python_value)
116+
117+
assert protobuf_value.WhichOneof("value") == "int32_value"
118+
assert protobuf_value.int32_value == 10
119+
assert protobuf_value.attributes["NI_UnitDescription"].string_value == "Volts"
120+
121+
122+
def test___float_scalar___convert___valid_double_scalar_protobuf() -> None:
123+
python_value = Scalar(20.0, "Volts")
124+
125+
protobuf_value = scalar_to_protobuf(python_value)
126+
127+
assert protobuf_value.WhichOneof("value") == "double_value"
128+
assert protobuf_value.double_value == 20.0
129+
assert protobuf_value.attributes["NI_UnitDescription"].string_value == "Volts"
130+
131+
132+
def test___str_scalar___convert___valid_string_scalar_protobuf() -> None:
133+
python_value = Scalar("value", "Volts")
134+
135+
protobuf_value = scalar_to_protobuf(python_value)
136+
137+
assert protobuf_value.WhichOneof("value") == "string_value"
138+
assert protobuf_value.string_value == "value"
139+
assert protobuf_value.attributes["NI_UnitDescription"].string_value == "Volts"
140+
141+
142+
def test___scalar_units_unset___convert___protobuf_units_blank() -> None:
143+
python_value = Scalar(10)
144+
145+
protobuf_value = scalar_to_protobuf(python_value)
146+
147+
assert protobuf_value.WhichOneof("value") == "int32_value"
148+
assert protobuf_value.int32_value == 10
149+
assert protobuf_value.attributes["NI_UnitDescription"].string_value == ""
150+
151+
152+
def test___non_units_attributes___to_protobuf___attributes_converted() -> None:
153+
python_value = Scalar("value", "Volts")
154+
python_value.extended_properties["NI_ChannelName"] = "Dev1/ai0"
155+
156+
protobuf_value = scalar_to_protobuf(python_value)
157+
158+
assert protobuf_value.WhichOneof("value") == "string_value"
159+
assert protobuf_value.string_value == "value"
160+
assert protobuf_value.attributes["NI_UnitDescription"].string_value == "Volts"
161+
assert protobuf_value.attributes["NI_ChannelName"].string_value == "Dev1/ai0"

0 commit comments

Comments
 (0)