Skip to content

Commit 40b68af

Browse files
authored
fix: Prevent string discrete values which are string of numbers from passing as numeric condition values. (#130)
1 parent a9640b9 commit 40b68af

File tree

2 files changed

+60
-6
lines changed

2 files changed

+60
-6
lines changed

nisystemlink/clients/spec/models/_condition.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import List, Optional, Union
33

44
from nisystemlink.clients.core._uplink._json_model import JsonModel
5+
from pydantic import StrictFloat, StrictInt, StrictStr
56

67

78
class ConditionType(Enum):
@@ -43,7 +44,11 @@ class NumericConditionValue(ConditionValueBase):
4344
range: Optional[List[ConditionRange]] = None
4445
"""List of condition range values."""
4546

46-
discrete: Optional[List[float]] = None
47+
# StrictFloat and StrictInt are used here because discrete is a common property for
48+
# NumericConditionValue and StringConditionValue.
49+
# If float/int is used, pydantic converts string of number into float/int by default
50+
# when deserializing a StringCondition JSON and misinterprets it as NumericConditionValue type.
51+
discrete: Optional[List[Union[StrictFloat, StrictInt]]] = None
4752
"""List of condition discrete values."""
4853

4954
unit: Optional[str] = None
@@ -56,7 +61,11 @@ class StringConditionValue(ConditionValueBase):
5661
String conditions may only contain discrete lists of values.
5762
"""
5863

59-
discrete: Optional[List[str]] = None
64+
# StrictStr is used here because discrete is a common property for
65+
# NumericConditionValue and StringConditionValue.
66+
# If str is used, pydantic converts any datatype into string by default when deserializing a
67+
# NumericCondition JSON and misinterprets it as StringConditionValue type.
68+
discrete: Optional[List[StrictStr]] = None
6069
"""List of condition discrete values."""
6170

6271

@@ -66,5 +75,8 @@ class Condition(JsonModel):
6675
name: Optional[str] = None
6776
"""Name of the condition."""
6877

78+
# Ideal approach is to set the dtype here as ConditionValue and use pydantic discriminator to
79+
# deserialize/serialize the JSON into correct ConditionValue sub types. But Union of dtypes is
80+
# used here as the discriminator field could be none when projection is used in query API.
6981
value: Optional[Union[NumericConditionValue, StringConditionValue]] = None
7082
"""Value of the condition."""

tests/integration/spec/test_spec.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
SpecificationLimit,
1818
SpecificationProjection,
1919
SpecificationType,
20+
StringConditionValue,
2021
UpdateSpecificationsRequest,
2122
UpdateSpecificationsRequestObject,
2223
)
@@ -83,15 +84,15 @@ def create_specs_for_query(create_specs, product):
8384
value=NumericConditionValue(
8485
condition_type=ConditionType.NUMERIC,
8586
range=[ConditionRange(min=-25, step=20, max=85)],
87+
discrete=[2, 1.5, 21],
8688
unit="C",
8789
),
8890
),
8991
Condition(
9092
name="Supply Voltage",
91-
value=NumericConditionValue(
92-
condition_type=ConditionType.NUMERIC,
93-
discrete=[1.3, 1.5, 1.7],
94-
unit="mV",
93+
value=StringConditionValue(
94+
condition_type=ConditionType.STRING,
95+
discrete=["1.3", "1.5", "1.7"],
9596
),
9697
),
9798
],
@@ -298,6 +299,47 @@ def test__query_spec_projection_columns__columns_returned(
298299
assert "spec_id" in spec_columns
299300
assert "name" in spec_columns
300301

302+
def test__query_specs__returns_condition_value_type_correctly(
303+
self, client: SpecClient, create_specs, create_specs_for_query, product
304+
):
305+
request = QuerySpecificationsRequest(
306+
product_ids=[product],
307+
projection=[
308+
SpecificationProjection.CONDITION_NAME,
309+
SpecificationProjection.CONDITION_UNIT,
310+
SpecificationProjection.CONDITION_VALUES,
311+
],
312+
)
313+
314+
response = client.query_specs(request)
315+
316+
assert response.specs
317+
assert len(response.specs) == 3
318+
condition_1 = (
319+
response.specs[1].conditions[0].value
320+
if response.specs[1].conditions
321+
else None
322+
)
323+
condition_2 = (
324+
response.specs[1].conditions[1].value
325+
if response.specs[1].conditions
326+
else None
327+
)
328+
condition_1_discrete_values = (
329+
[discrete for discrete in condition_1.discrete or []] if condition_1 else []
330+
)
331+
condition_2_discrete_values = (
332+
[discrete for discrete in condition_2.discrete or []] if condition_2 else []
333+
)
334+
assert isinstance(condition_1, NumericConditionValue)
335+
assert isinstance(condition_2, StringConditionValue)
336+
assert isinstance(condition_1_discrete_values[0], int)
337+
assert isinstance(condition_1_discrete_values[1], float)
338+
assert isinstance(condition_1_discrete_values[2], int)
339+
assert isinstance(condition_2_discrete_values[0], str)
340+
assert isinstance(condition_2_discrete_values[1], str)
341+
assert isinstance(condition_2_discrete_values[2], str)
342+
301343
def test__without_condition_type_projection__query_specs__condition_type_field_is_unset(
302344
self, client: SpecClient, create_specs, create_specs_for_query, product
303345
):

0 commit comments

Comments
 (0)