Skip to content

Commit db62a56

Browse files
mjohanse-emrMichael Johansen
andauthored
Create a Vector python type with unit tests. (#161)
* Create a Vector python type with unit tests. Signed-off-by: Michael Johansen <[email protected]> * Refactor Vector so that it subclasses MutableSequence. Signed-off-by: Michael Johansen <[email protected]> * Get rid of __init__.py and move into public src/nitypes/vector.py. Signed-off-by: Michael Johansen <[email protected]> * Add/update unit tests based on PR feedback. Signed-off-by: Michael Johansen <[email protected]> * Import based on TYPE_CHECKING to keep private modules out of the docs. Signed-off-by: Michael Johansen <[email protected]> * Rename values_type to value_type. Make units settable. Other PR feedback. Signed-off-by: Michael Johansen <[email protected]> * Change the typevar name. Get rid of int bounds checking. Clean up type match checks in set_item. Signed-off-by: Michael Johansen <[email protected]> * Change from Any to object based on PR feedback. Signed-off-by: Michael Johansen <[email protected]> --------- Signed-off-by: Michael Johansen <[email protected]> Co-authored-by: Michael Johansen <[email protected]>
1 parent 2b853b4 commit db62a56

File tree

5 files changed

+626
-0
lines changed

5 files changed

+626
-0
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,12 @@ in the API Reference.
133133
information and extended properties. Valid types for the scalar value are `bool`, `int`, `float`,
134134
and `str`. For more details, see
135135
[Scalar](https://nitypes.readthedocs.io/en/latest/autoapi/nitypes/scalar/index.html#scalar) in the
136+
API Reference.
137+
138+
## Vector
139+
140+
`nitypes.vector.Vector` is a data type that represents an array of scalar values with units
141+
information and extended properties. Valid types for the scalar values are `bool`, `int`, `float`,
142+
and `str`. For more details, see
143+
[Scalar](https://nitypes.readthedocs.io/en/latest/autoapi/nitypes/vector/index.html#vector) in the
136144
API Reference.

docs/intro.inc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,10 @@ Scalar
107107
:class:`Scalar` is a data type that represents a single scalar value with units
108108
information. Valid types for the scalar value are :any:`bool`, :any:`int`,
109109
:any:`float`, and :any:`str`.
110+
111+
Vector
112+
^^^^^^
113+
114+
:class:`Vector` is a data type that represents an array of scalar values with units
115+
information. Valid types for the scalar values are :any:`bool`, :any:`int`,
116+
:any:`float`, and :any:`str`.

src/nitypes/vector.py

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
"""Vector data type for NI Python APIs.
2+
3+
Vector Data Type
4+
=================
5+
:class:`Vector`: A vector data object represents an array of scalar values with units information.
6+
Valid types for the scalar value are :any:`bool`, :any:`int`, :any:`float`, and :any:`str`.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from collections.abc import Iterable, MutableSequence
12+
from typing import TYPE_CHECKING, overload, Any, Union
13+
14+
from typing_extensions import TypeVar, final, override
15+
16+
from nitypes._exceptions import invalid_arg_type
17+
from nitypes.waveform._extended_properties import UNIT_DESCRIPTION
18+
19+
if TYPE_CHECKING:
20+
# Import from the public package so the docs don't reference private submodules.
21+
from nitypes.waveform import ExtendedPropertyDictionary
22+
else:
23+
from nitypes.waveform._extended_properties import ExtendedPropertyDictionary
24+
25+
TScalar = TypeVar("TScalar", bound=Union[bool, int, float, str])
26+
27+
28+
@final
29+
class Vector(MutableSequence[TScalar]):
30+
"""A sequence of scalar values with units information.
31+
32+
Constructing
33+
^^^^^^^^^^^^
34+
35+
To construct a vector data object, use the :class:`Vector` class:
36+
37+
>>> Vector([False, True])
38+
nitypes.vector.Vector(values=[False, True], units='')
39+
>>> Vector([0, 1, 2])
40+
nitypes.vector.Vector(values=[0, 1, 2], units='')
41+
>>> Vector([5.0, 6.0], 'volts')
42+
nitypes.vector.Vector(values=[5.0, 6.0], units='volts')
43+
>>> Vector(["one", "two"], "volts")
44+
nitypes.vector.Vector(values=['one', 'two'], units='volts')
45+
"""
46+
47+
__slots__ = [
48+
"_values",
49+
"_value_type",
50+
"_extended_properties",
51+
]
52+
53+
_values: list[TScalar]
54+
_value_type: type[TScalar]
55+
_extended_properties: ExtendedPropertyDictionary
56+
57+
def __init__(
58+
self,
59+
values: Iterable[TScalar],
60+
units: str = "",
61+
*,
62+
value_type: type[TScalar] | None = None,
63+
) -> None:
64+
"""Initialize a new vector.
65+
66+
Args:
67+
values: The scalar values to store in this object.
68+
units: The units string associated with this data.
69+
value_type: The type of values that will be added to this Vector.
70+
This parameter should only be used when creating a Vector with
71+
an empty Iterable.
72+
73+
Returns:
74+
A vector data object.
75+
"""
76+
if not values:
77+
if not value_type:
78+
raise TypeError("You must specify values as non-empty or specify value_type.")
79+
self._value_type = value_type
80+
else:
81+
# Validate the values input
82+
for index, value in enumerate(values):
83+
# Only set _value_type once.
84+
if not index:
85+
self._value_type = type(value)
86+
87+
if not isinstance(value, (bool, int, float, str)):
88+
raise invalid_arg_type("vector input data", "bool, int, float, or str", values)
89+
90+
if not isinstance(value, self._value_type):
91+
raise TypeError("All values in the values input must be of the same type.")
92+
93+
if not isinstance(units, str):
94+
raise invalid_arg_type("units", "str", units)
95+
96+
self._values = list(values)
97+
self._extended_properties = ExtendedPropertyDictionary()
98+
self._extended_properties[UNIT_DESCRIPTION] = units
99+
100+
@property
101+
def units(self) -> str:
102+
"""The unit of measurement, such as volts, of the vector."""
103+
value = self._extended_properties.get(UNIT_DESCRIPTION, "")
104+
assert isinstance(value, str)
105+
return value
106+
107+
@units.setter
108+
def units(self, value: str) -> None:
109+
if not isinstance(value, str):
110+
raise invalid_arg_type("units", "str", value)
111+
self._extended_properties[UNIT_DESCRIPTION] = value
112+
113+
@property
114+
def extended_properties(self) -> ExtendedPropertyDictionary:
115+
"""The extended properties for the vector.
116+
117+
.. note::
118+
Data stored in the extended properties dictionary may not be encrypted when you send it
119+
over the network or write it to a TDMS file.
120+
"""
121+
return self._extended_properties
122+
123+
@overload
124+
def __getitem__( # noqa: D105 - missing docstring in magic method
125+
self, index: int
126+
) -> TScalar: ...
127+
128+
@overload
129+
def __getitem__( # noqa: D105 - missing docstring in magic method
130+
self, index: slice
131+
) -> MutableSequence[TScalar]: ...
132+
133+
@override
134+
def __getitem__(self, index: int | slice) -> TScalar | MutableSequence[TScalar]:
135+
"""Return the TimeDelta at the specified location."""
136+
return self._values[index]
137+
138+
@overload
139+
def __setitem__( # noqa: D105 - missing docstring in magic method
140+
self, index: int, value: TScalar
141+
) -> None: ...
142+
143+
@overload
144+
def __setitem__( # noqa: D105 - missing docstring in magic method
145+
self, index: slice, value: Iterable[TScalar]
146+
) -> None: ...
147+
148+
@override
149+
def __setitem__(self, index: int | slice, value: TScalar | Iterable[TScalar]) -> None:
150+
"""Set value(s) at the specified location."""
151+
if isinstance(index, int):
152+
if isinstance(value, Iterable) and not isinstance(value, str):
153+
raise TypeError("You cannot assign an Iterable to a vector index.")
154+
elif not isinstance(value, self._value_type):
155+
raise self._create_value_mismatch_exception(value)
156+
157+
self._values[index] = value
158+
else: # slice
159+
if not isinstance(value, Iterable):
160+
raise TypeError("You must assign an Iterable to a Vector slice.")
161+
elif isinstance(value, str): # Narrow the type to exclude string.
162+
raise TypeError("You cannot assign a string to Vector slice.")
163+
else:
164+
# Assigning an empty Iterable to a slice is valid, so we don't check for empty.
165+
# If an empty Iterable is assigned to a slice, that slice is deleted.
166+
for subval in value:
167+
if not isinstance(subval, self._value_type):
168+
raise self._create_value_mismatch_exception(subval)
169+
170+
self._values[index] = value
171+
172+
def __delitem__(self, index: int | slice) -> None:
173+
"""Delete item(s) from the specified location."""
174+
del self._values[index]
175+
176+
def __len__(self) -> int:
177+
"""Return the length of the Vector."""
178+
return len(self._values)
179+
180+
def insert(self, index: int, value: TScalar) -> None:
181+
"""Insert a value at the specified location."""
182+
if not isinstance(value, self._value_type):
183+
raise self._create_value_mismatch_exception(value)
184+
self._values.insert(index, value)
185+
186+
def __eq__(self, value: object, /) -> bool:
187+
"""Return self==value."""
188+
if not isinstance(value, self.__class__):
189+
return NotImplemented
190+
return self._values == value._values and self.units == value.units
191+
192+
def __reduce__(self) -> tuple[Any, ...]:
193+
"""Return object state for pickling."""
194+
return (self.__class__, (self._values, self.units))
195+
196+
def __repr__(self) -> str:
197+
"""Return repr(self)."""
198+
args = [f"values={self._values!r}", f"units={self.units!r}"]
199+
return f"{self.__class__.__module__}.{self.__class__.__name__}({', '.join(args)})"
200+
201+
def __str__(self) -> str:
202+
"""Return str(self)."""
203+
if self.units:
204+
values_with_units = [f"{value} {self.units}" for value in self._values]
205+
comma_delimited_str = ", ".join(values_with_units)
206+
else:
207+
values = [f"{value}" for value in self._values]
208+
comma_delimited_str = ", ".join(values)
209+
210+
return f"[{comma_delimited_str}]"
211+
212+
def _create_value_mismatch_exception(self, value: object) -> TypeError:
213+
return TypeError(
214+
f"Input type does not match existing type. Input Type: {type(value)} "
215+
f"Existing Type: {self._value_type}"
216+
)

tests/unit/vector/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Unit tests for the nitypes.vector package."""

0 commit comments

Comments
 (0)