Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 13 additions & 18 deletions src/nitypes/_arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import numpy as np
import numpy.typing as npt

from nitypes._exceptions import invalid_arg_type, invalid_arg_value, unsupported_arg


def arg_to_float(
arg_description: str, value: SupportsFloat | None, default_value: float | None = None
Expand Down Expand Up @@ -39,21 +41,15 @@ def arg_to_float(
"""
if value is None:
if default_value is None:
raise TypeError(
f"The {arg_description} must be a floating point number.\n\n"
f"Provided value: {value!r}"
)
raise invalid_arg_type(arg_description, "floating point number", value)
value = default_value

if not isinstance(value, float):
try:
# Use value.__float__() because float(value) also accepts strings.
return value.__float__()
except Exception:
raise TypeError(
f"The {arg_description} must be a floating point number.\n\n"
f"Provided value: {value!r}"
) from None
raise invalid_arg_type(arg_description, "floating point number", value) from None

return value

Expand Down Expand Up @@ -90,18 +86,14 @@ def arg_to_int(
"""
if value is None:
if default_value is None:
raise TypeError(
f"The {arg_description} must be an integer.\n\n" f"Provided value: {value!r}"
)
raise invalid_arg_type(arg_description, "integer", value)
value = default_value

if not isinstance(value, int):
try:
return operator.index(value)
except Exception:
raise TypeError(
f"The {arg_description} must be an integer.\n\n" f"Provided value: {value!r}"
) from None
raise invalid_arg_type(arg_description, "integer", value) from None

return value

Expand Down Expand Up @@ -132,10 +124,7 @@ def arg_to_uint(
"""
value = arg_to_int(arg_description, value, default_value)
if value < 0:
raise ValueError(
f"The {arg_description} must be a non-negative integer.\n\n"
f"Provided value: {value!r}"
)
raise invalid_arg_value(arg_description, "non-negative integer", value)
return value


Expand Down Expand Up @@ -163,3 +152,9 @@ def validate_dtype(dtype: npt.DTypeLike, supported_dtypes: tuple[npt.DTypeLike,
f"Data type: {np.dtype(dtype)}\n"
f"Supported data types: {', '.join(supported_dtype_names)}"
)


def validate_unsupported_arg(arg_description: str, value: object) -> None:
"""Validate that an unsupported argument is None."""
if value is not None:
raise unsupported_arg(arg_description, value)
52 changes: 52 additions & 0 deletions src/nitypes/_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import reprlib
import sys


Expand All @@ -21,3 +22,54 @@ def add_note(exception: Exception, note: str) -> None:
else:
message = exception.args[0] + "\n" + note
exception.args = (message,) + exception.args[1:]


def invalid_arg_value(
arg_description: str, valid_value_description: str, value: object
) -> ValueError:
"""Create a ValueError for an invalid argument value."""
return ValueError(
f"The {arg_description} must be {_a(valid_value_description)}.\n\n"
f"Provided value: {reprlib.repr(value)}"
)


def invalid_arg_type(arg_description: str, type_description: str, value: object) -> TypeError:
"""Create a TypeError for an invalid argument type."""
return TypeError(
f"The {arg_description} must be {_a(type_description)}.\n\n"
f"Provided value: {reprlib.repr(value)}"
)


def invalid_array_ndim(arg_description: str, valid_value_description: str, ndim: int) -> ValueError:
"""Create a ValueError for an array with an invalid number of dimensions."""
raise ValueError(
f"The {arg_description} must be {_a(valid_value_description)}.\n\n"
f"Number of dimensions: {ndim}"
)


def invalid_requested_type(type_description: str, requested_type: type) -> TypeError:
"""Create a TypeError for an invalid requested type."""
raise TypeError(
f"The requested type must be {_a(type_description)} type.\n\n"
f"Requested type: {requested_type}"
)


def unsupported_arg(arg_description: str, value: object) -> ValueError:
"""Create a ValueError for an unsupported argument."""
raise ValueError(
f"The {arg_description} argument is not supported.\n\n"
f"Provided value: {reprlib.repr(value)}"
)


# English-specific hack. This is why we prefer "Key: value" for localizable errors. TODO: consider
# moving the full strings into a string table instead of building them out of English noun phrases.
def _a(noun: str) -> str:
indefinite_article = "an" if noun[0] in "AEIOUaeiou" else "a"
if noun.startswith("one-"):
indefinite_article = "a"
return f"{indefinite_article} {noun}"
18 changes: 8 additions & 10 deletions src/nitypes/time/_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import hightime as ht

from nitypes._exceptions import invalid_arg_type, invalid_requested_type

if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
Expand All @@ -24,15 +26,13 @@ def convert_datetime(requested_type: type[_TDateTime], value: _AnyDateTime, /) -
"""Convert a datetime object to the specified type."""
convert_func = _CONVERT_DATETIME_FOR_TYPE.get(requested_type)
if convert_func is None:
raise TypeError(
"The requested type must be a datetime type.\n\n" f"Requested type: {requested_type}"
)
raise invalid_requested_type("datetime", requested_type)
return cast(_TDateTime, convert_func(value))


@singledispatch
def _convert_to_dt_datetime(value: object, /) -> dt.datetime:
raise TypeError("The value must be a datetime.\n\n" f"Provided value: {value!r}")
raise invalid_arg_type("value", "datetime", value)


@_convert_to_dt_datetime.register
Expand All @@ -57,7 +57,7 @@ def _(value: ht.datetime, /) -> dt.datetime:

@singledispatch
def _convert_to_ht_datetime(value: object, /) -> ht.datetime:
raise TypeError("The value must be a datetime.\n\n" f"Provided value: {value!r}")
raise invalid_arg_type("value", "datetime", value)


@_convert_to_ht_datetime.register
Expand Down Expand Up @@ -90,15 +90,13 @@ def convert_timedelta(requested_type: type[_TTimeDelta], value: _AnyTimeDelta, /
"""Convert a timedelta object to the specified type."""
convert_func = _CONVERT_TIMEDELTA_FOR_TYPE.get(requested_type)
if convert_func is None:
raise TypeError(
"The requested type must be a timedelta type.\n\n" f"Requested type: {requested_type}"
)
raise invalid_requested_type("timedelta", requested_type)
return cast(_TTimeDelta, convert_func(value))


@singledispatch
def _convert_to_dt_timedelta(value: object, /) -> dt.timedelta:
raise TypeError("The value must be a timedelta.\n\n" f"Provided value: {value!r}")
raise invalid_arg_type("value", "timedelta", value)


@_convert_to_dt_timedelta.register
Expand All @@ -113,7 +111,7 @@ def _(value: ht.timedelta, /) -> dt.timedelta:

@singledispatch
def _convert_to_ht_timedelta(value: object, /) -> ht.timedelta:
raise TypeError("The value must be a timedelta.\n\n" f"Provided value: {value!r}")
raise invalid_arg_type("value", "timedelta", value)


@_convert_to_ht_timedelta.register
Expand Down
46 changes: 20 additions & 26 deletions src/nitypes/waveform/_analog_waveform.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import numpy.typing as npt

from nitypes._arguments import arg_to_uint, validate_dtype
from nitypes._exceptions import invalid_arg_type, invalid_array_ndim
from nitypes.waveform._extended_properties import (
CHANNEL_NAME,
UNIT_DESCRIPTION,
Expand Down Expand Up @@ -124,18 +125,16 @@ def from_array_1d(
"""
if isinstance(array, np.ndarray):
if array.ndim != 1:
raise ValueError(
f"The input array must be a one-dimensional array or sequence.\n\nNumber of dimensions: {array.ndim}"
raise invalid_array_ndim(
"input array", "one-dimensional array or sequence", array.ndim
)
elif isinstance(array, Sequence) or (
sys.version_info < (3, 10) and isinstance(array, std_array.array)
):
if dtype is None:
raise ValueError("You must specify a dtype when the input array is a sequence.")
else:
raise TypeError(
f"The input array must be a one-dimensional array or sequence.\n\nType: {type(array)}"
)
raise invalid_arg_type("input array", "one-dimensional array or sequence", array)

return AnalogWaveform(
_data=np.asarray(array, dtype, copy=copy),
Expand Down Expand Up @@ -200,18 +199,16 @@ def from_array_2d(
"""
if isinstance(array, np.ndarray):
if array.ndim != 2:
raise ValueError(
f"The input array must be a two-dimensional array or nested sequence.\n\nNumber of dimensions: {array.ndim}"
raise invalid_array_ndim(
"input array", "two-dimensional array or nested sequence", array.ndim
)
elif isinstance(array, Sequence) or (
sys.version_info < (3, 10) and isinstance(array, std_array.array)
):
if dtype is None:
raise ValueError("You must specify a dtype when the input array is a sequence.")
else:
raise TypeError(
f"The input array must be a two-dimensional array or nested sequence.\n\nType: {type(array)}"
)
raise invalid_arg_type("input array", "two-dimensional array or nested sequence", array)

return [
AnalogWaveform(
Expand Down Expand Up @@ -369,16 +366,18 @@ def _init_with_provided_array(
capacity: SupportsIndex | None = None,
) -> None:
if not isinstance(data, np.ndarray):
raise TypeError("The input array must be a one-dimensional array.")
raise invalid_arg_type("input array", "one-dimensional array", data)
if data.ndim != 1:
raise ValueError("The input array must be a one-dimensional array.")
raise invalid_array_ndim("input array", "one-dimensional array", data.ndim)

if dtype is None:
dtype = data.dtype
if dtype != data.dtype:
# from_array_1d() converts the input array to the requested dtype, so this error may be
# unreachable.
raise TypeError("The data type of the input array must match the requested data type.")
raise TypeError(
"The data type of the input array must match the requested data type.\n\n"
f"Array data type: {data.dtype}\n"
f"Requested data type: {np.dtype(dtype)}"
)
validate_dtype(dtype, _ANALOG_DTYPES)

capacity = arg_to_uint("capacity", capacity, len(data))
Expand Down Expand Up @@ -530,10 +529,7 @@ def capacity(self) -> int:

@capacity.setter
def capacity(self, value: int) -> None:
if value < 0:
raise ValueError(
"The capacity must be a non-negative integer.\n\n" f"Capacity: {value}"
)
value = arg_to_uint("capacity", value)
if value < self._start_index + self._sample_count:
raise ValueError(
"The capacity must be equal to or greater than the number of samples in the waveform.\n\n"
Expand Down Expand Up @@ -563,7 +559,7 @@ def channel_name(self) -> str:
@channel_name.setter
def channel_name(self, value: str) -> None:
if not isinstance(value, str):
raise TypeError("The channel name must be a str.\n\n" f"Provided value: {value!r}")
raise invalid_arg_type("channel name", "str", value)
self._extended_properties[CHANNEL_NAME] = value

@property
Expand All @@ -576,7 +572,7 @@ def unit_description(self) -> str:
@unit_description.setter
def unit_description(self, value: str) -> None:
if not isinstance(value, str):
raise TypeError("The unit description must be a str.\n\n" f"Provided value: {value!r}")
raise invalid_arg_type("unit description", "str", value)
self._extended_properties[UNIT_DESCRIPTION] = value

@property
Expand All @@ -597,7 +593,7 @@ def timing(self) -> Timing:
@timing.setter
def timing(self, value: Timing) -> None:
if not isinstance(value, Timing):
raise TypeError("The timing information must be a Timing object.")
raise invalid_arg_type("timing information", "Timing object", value)
self._timing = value
self._precision_timing = None

Expand Down Expand Up @@ -634,7 +630,7 @@ def precision_timing(self) -> PrecisionTiming:
@precision_timing.setter
def precision_timing(self, value: PrecisionTiming) -> None:
if not isinstance(value, PrecisionTiming):
raise TypeError("The precision timing information must be a PrecisionTiming object.")
raise invalid_arg_type("precision timing information", "PrecisionTiming object", value)
self._precision_timing = value
self._timing = None

Expand All @@ -646,7 +642,5 @@ def scale_mode(self) -> ScaleMode:
@scale_mode.setter
def scale_mode(self, value: ScaleMode) -> None:
if not isinstance(value, ScaleMode):
raise TypeError(
"The scale mode must be a ScaleMode object.\n\n" f"Provided value: {value!r}"
)
raise invalid_arg_type("scale mode", "ScaleMode object", value)
self._scale_mode = value
Loading