From 8aa47b83530e6deec578372ee8ea4ed4a0c364ff Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Thu, 28 May 2026 13:28:34 -0500 Subject: [PATCH 1/4] Update __init__ and __setitem__ in Vector to support Iterables that can only be iterated once. Signed-off-by: Hunter Kennon hunter.kennon@emerson.com --- src/nitypes/vector.py | 37 +++++++++++---------- tests/unit/vector/test_vector.py | 55 ++++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 18 deletions(-) diff --git a/src/nitypes/vector.py b/src/nitypes/vector.py index ff719c4d..efeba107 100644 --- a/src/nitypes/vector.py +++ b/src/nitypes/vector.py @@ -8,7 +8,7 @@ from __future__ import annotations -from collections.abc import Iterable, Mapping, MutableSequence +from collections.abc import Iterable, Mapping, MutableSequence, Sequence from typing import TYPE_CHECKING, Any, Union, overload from typing_extensions import Self, TypeVar, final, override @@ -79,19 +79,18 @@ def __init__( Returns: A vector data object. """ - if not values: + backing_values = list(values) + if not backing_values: if not value_type: raise TypeError("You must specify values as non-empty or specify value_type.") self._value_type = value_type else: - # Validate the values input - for index, value in enumerate(values): - # Only set _value_type once. - if not index: - self._value_type = type(value) - + self._value_type = type(backing_values[0]) + for value in backing_values: if not isinstance(value, (bool, int, float, str)): - raise invalid_arg_type("vector input data", "bool, int, float, or str", values) + raise invalid_arg_type( + "vector input data", "bool, int, float, or str", backing_values + ) if not isinstance(value, self._value_type): raise TypeError("All values in the values input must be of the same type.") @@ -99,7 +98,7 @@ def __init__( if not isinstance(units, str): raise invalid_arg_type("units", "str", units) - self._values = list(values) + self._values = backing_values if copy_extended_properties or not isinstance( extended_properties, ExtendedPropertyDictionary ): @@ -178,14 +177,20 @@ def __setitem__(self, index: int | slice, value: TScalar | Iterable[TScalar]) -> raise TypeError("You must assign an Iterable to a Vector slice.") elif isinstance(value, str): # Narrow the type to exclude string. raise TypeError("You cannot assign a string to Vector slice.") + + if isinstance(value, Sequence): + # A Sequence can be iterated over multiple times, so no need for a wrapper list. + replacement_values = value else: - # Assigning an empty Iterable to a slice is valid, so we don't check for empty. - # If an empty Iterable is assigned to a slice, that slice is deleted. - for subval in value: - if not isinstance(subval, self._value_type): - raise self._create_value_mismatch_exception(subval) + replacement_values = list(value) - self._values[index] = value + # Assigning an empty Iterable to a slice is valid, so we don't check for empty. + # If an empty Iterable is assigned to a slice, that slice is deleted. + for subval in replacement_values: + if not isinstance(subval, self._value_type): + raise self._create_value_mismatch_exception(subval) + + self._values[index] = replacement_values def __delitem__(self, index: int | slice) -> None: """Delete item(s) from the specified location.""" diff --git a/tests/unit/vector/test_vector.py b/tests/unit/vector/test_vector.py index 65cced64..59f1dc36 100644 --- a/tests/unit/vector/test_vector.py +++ b/tests/unit/vector/test_vector.py @@ -2,6 +2,7 @@ import copy import pickle +from collections.abc import Iterable from typing import Any import pytest @@ -51,7 +52,7 @@ def test___int_data_values___create___creates_with_int_data_and_default_units() assert data.units == "" -def test___float_data_value___create___creates_scalar_data_with_data_and_default_units() -> None: +def test___float_data_values___create___creates_with_float_data_and_default_units() -> None: data = Vector([20.2, 30.3, 40.4]) assert_type(data._values[0], float) @@ -59,7 +60,7 @@ def test___float_data_value___create___creates_scalar_data_with_data_and_default assert data.units == "" -def test___str_data_value___create___creates_scalar_data_with_data_and_default_units() -> None: +def test___str_data_values___create___creates_with_str_data_and_default_units() -> None: data = Vector(["one", "two"]) assert_type(data._values[0], str) @@ -67,6 +68,19 @@ def test___str_data_value___create___creates_scalar_data_with_data_and_default_u assert data.units == "" +def test___generator_data_values___create___creates_with_data_and_default_units() -> None: + def get_values() -> Iterable[float]: + yield 20.2 + yield 30.3 + yield 40.4 + + data = Vector(get_values()) + + assert_type(data._values[0], float) + assert data._values == [20.2, 30.3, 40.4] + assert data.units == "" + + @pytest.mark.parametrize("data_value", [True, 10, 20.0, "value"]) @pytest.mark.parametrize("units", ["volts"]) def test___data_value_and_units___create___creates_scalar_data_with_data_and_units( @@ -101,6 +115,29 @@ def test___invalid_data_value___create___raises_type_error(data_value: Any) -> N assert exc.value.args[0].startswith("The vector input data must be a bool, int, float, or str.") +def test___invalid_generator_data_values___create___raises_type_error() -> None: + def get_values() -> Iterable[Any]: + yield from [{"test_key", "test_value"}, 1.0, 2.0] + + with pytest.raises(TypeError) as exc: + _ = Vector(get_values()) + + assert exc.value.args[0].startswith("The vector input data must be a bool, int, float, or str.") + assert "{'test_key', 'test_value'}" in exc.value.args[0] + + +def test___empty_generator_data_values___create___raises_type_error() -> None: + def get_values() -> Iterable[float]: + yield from [] + + with pytest.raises(TypeError) as exc: + _ = Vector(get_values()) + + assert exc.value.args[0].startswith( + "You must specify values as non-empty or specify value_type." + ) + + def test___mixed_data_values___create___raises_type_error() -> None: with pytest.raises(TypeError) as exc: _ = Vector([True, "string", 1.0]) @@ -143,6 +180,19 @@ def test___vector_with_data___set_item_at_slice___values_set_correctly() -> None vector = Vector([1, 2, 3], "volts") vector[0:2] = [6, 7] + + assert vector._values == [6, 7, 3] + + +def test___vector_with_data___set_item_at_slice_with_generator___values_set_correctly() -> None: + def get_values() -> Iterable[int]: + yield 6 + yield 7 + + vector = Vector([1, 2, 3], "volts") + + vector[0:2] = get_values() + assert vector._values == [6, 7, 3] @@ -150,6 +200,7 @@ def test___vector_with_data___set_item_at_slice_to_empty_list___values_set_corre vector = Vector([1, 2, 3], "volts") vector[0:2] = [] + assert vector._values == [3] From cd9f4311d2cbc06130a0a24fdf173a8a2e5a2e64 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Thu, 28 May 2026 16:09:31 -0500 Subject: [PATCH 2/4] Adding back code comment Signed-off-by: Hunter Kennon hunter.kennon@emerson.com --- src/nitypes/vector.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nitypes/vector.py b/src/nitypes/vector.py index efeba107..bbb96548 100644 --- a/src/nitypes/vector.py +++ b/src/nitypes/vector.py @@ -86,6 +86,7 @@ def __init__( self._value_type = value_type else: self._value_type = type(backing_values[0]) + # Validate the values input for value in backing_values: if not isinstance(value, (bool, int, float, str)): raise invalid_arg_type( From 402d87ea74aee23dc1dafb90248de2bbedbc5539 Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Thu, 28 May 2026 16:12:50 -0500 Subject: [PATCH 3/4] Review feedback - Do not predicate test assertion on assumption about order of set elements in string representation Signed-off-by: Hunter Kennon hunter.kennon@emerson.com --- tests/unit/vector/test_vector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/vector/test_vector.py b/tests/unit/vector/test_vector.py index 59f1dc36..0e669e8e 100644 --- a/tests/unit/vector/test_vector.py +++ b/tests/unit/vector/test_vector.py @@ -117,13 +117,13 @@ def test___invalid_data_value___create___raises_type_error(data_value: Any) -> N def test___invalid_generator_data_values___create___raises_type_error() -> None: def get_values() -> Iterable[Any]: - yield from [{"test_key", "test_value"}, 1.0, 2.0] + yield from [{"value_one", "value_two"}, 1.0, 2.0] with pytest.raises(TypeError) as exc: _ = Vector(get_values()) assert exc.value.args[0].startswith("The vector input data must be a bool, int, float, or str.") - assert "{'test_key', 'test_value'}" in exc.value.args[0] + assert "value_one" in exc.value.args[0] def test___empty_generator_data_values___create___raises_type_error() -> None: From c6b51ad674018b569dc1282967fefb51e04c139f Mon Sep 17 00:00:00 2001 From: hunter-ni <68388297+hunter-ni@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:37:43 -0500 Subject: [PATCH 4/4] Review feedback - Use more specific type hints in tests Signed-off-by: Hunter Kennon hunter.kennon@emerson.com --- tests/unit/vector/test_vector.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/vector/test_vector.py b/tests/unit/vector/test_vector.py index 0e669e8e..6f4e68c8 100644 --- a/tests/unit/vector/test_vector.py +++ b/tests/unit/vector/test_vector.py @@ -2,7 +2,7 @@ import copy import pickle -from collections.abc import Iterable +from collections.abc import Generator from typing import Any import pytest @@ -69,7 +69,7 @@ def test___str_data_values___create___creates_with_str_data_and_default_units() def test___generator_data_values___create___creates_with_data_and_default_units() -> None: - def get_values() -> Iterable[float]: + def get_values() -> Generator[float]: yield 20.2 yield 30.3 yield 40.4 @@ -116,7 +116,7 @@ def test___invalid_data_value___create___raises_type_error(data_value: Any) -> N def test___invalid_generator_data_values___create___raises_type_error() -> None: - def get_values() -> Iterable[Any]: + def get_values() -> Generator[Any]: yield from [{"value_one", "value_two"}, 1.0, 2.0] with pytest.raises(TypeError) as exc: @@ -127,7 +127,7 @@ def get_values() -> Iterable[Any]: def test___empty_generator_data_values___create___raises_type_error() -> None: - def get_values() -> Iterable[float]: + def get_values() -> Generator[float]: yield from [] with pytest.raises(TypeError) as exc: @@ -185,7 +185,7 @@ def test___vector_with_data___set_item_at_slice___values_set_correctly() -> None def test___vector_with_data___set_item_at_slice_with_generator___values_set_correctly() -> None: - def get_values() -> Iterable[int]: + def get_values() -> Generator[int]: yield 6 yield 7