Skip to content

Commit ad552a3

Browse files
authored
nitypes: Refactor error messages (#9)
* nitypes: Refactor error messages * exceptions: Add more exception functions
1 parent b8b14b8 commit ad552a3

File tree

6 files changed

+111
-96
lines changed

6 files changed

+111
-96
lines changed

src/nitypes/_arguments.py

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import numpy as np
77
import numpy.typing as npt
88

9+
from nitypes._exceptions import invalid_arg_type, invalid_arg_value, unsupported_arg
10+
911

1012
def arg_to_float(
1113
arg_description: str, value: SupportsFloat | None, default_value: float | None = None
@@ -39,21 +41,15 @@ def arg_to_float(
3941
"""
4042
if value is None:
4143
if default_value is None:
42-
raise TypeError(
43-
f"The {arg_description} must be a floating point number.\n\n"
44-
f"Provided value: {value!r}"
45-
)
44+
raise invalid_arg_type(arg_description, "floating point number", value)
4645
value = default_value
4746

4847
if not isinstance(value, float):
4948
try:
5049
# Use value.__float__() because float(value) also accepts strings.
5150
return value.__float__()
5251
except Exception:
53-
raise TypeError(
54-
f"The {arg_description} must be a floating point number.\n\n"
55-
f"Provided value: {value!r}"
56-
) from None
52+
raise invalid_arg_type(arg_description, "floating point number", value) from None
5753

5854
return value
5955

@@ -90,18 +86,14 @@ def arg_to_int(
9086
"""
9187
if value is None:
9288
if default_value is None:
93-
raise TypeError(
94-
f"The {arg_description} must be an integer.\n\n" f"Provided value: {value!r}"
95-
)
89+
raise invalid_arg_type(arg_description, "integer", value)
9690
value = default_value
9791

9892
if not isinstance(value, int):
9993
try:
10094
return operator.index(value)
10195
except Exception:
102-
raise TypeError(
103-
f"The {arg_description} must be an integer.\n\n" f"Provided value: {value!r}"
104-
) from None
96+
raise invalid_arg_type(arg_description, "integer", value) from None
10597

10698
return value
10799

@@ -132,10 +124,7 @@ def arg_to_uint(
132124
"""
133125
value = arg_to_int(arg_description, value, default_value)
134126
if value < 0:
135-
raise ValueError(
136-
f"The {arg_description} must be a non-negative integer.\n\n"
137-
f"Provided value: {value!r}"
138-
)
127+
raise invalid_arg_value(arg_description, "non-negative integer", value)
139128
return value
140129

141130

@@ -163,3 +152,9 @@ def validate_dtype(dtype: npt.DTypeLike, supported_dtypes: tuple[npt.DTypeLike,
163152
f"Data type: {np.dtype(dtype)}\n"
164153
f"Supported data types: {', '.join(supported_dtype_names)}"
165154
)
155+
156+
157+
def validate_unsupported_arg(arg_description: str, value: object) -> None:
158+
"""Validate that an unsupported argument is None."""
159+
if value is not None:
160+
raise unsupported_arg(arg_description, value)

src/nitypes/_exceptions.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import reprlib
34
import sys
45

56

@@ -21,3 +22,54 @@ def add_note(exception: Exception, note: str) -> None:
2122
else:
2223
message = exception.args[0] + "\n" + note
2324
exception.args = (message,) + exception.args[1:]
25+
26+
27+
def invalid_arg_value(
28+
arg_description: str, valid_value_description: str, value: object
29+
) -> ValueError:
30+
"""Create a ValueError for an invalid argument value."""
31+
return ValueError(
32+
f"The {arg_description} must be {_a(valid_value_description)}.\n\n"
33+
f"Provided value: {reprlib.repr(value)}"
34+
)
35+
36+
37+
def invalid_arg_type(arg_description: str, type_description: str, value: object) -> TypeError:
38+
"""Create a TypeError for an invalid argument type."""
39+
return TypeError(
40+
f"The {arg_description} must be {_a(type_description)}.\n\n"
41+
f"Provided value: {reprlib.repr(value)}"
42+
)
43+
44+
45+
def invalid_array_ndim(arg_description: str, valid_value_description: str, ndim: int) -> ValueError:
46+
"""Create a ValueError for an array with an invalid number of dimensions."""
47+
raise ValueError(
48+
f"The {arg_description} must be {_a(valid_value_description)}.\n\n"
49+
f"Number of dimensions: {ndim}"
50+
)
51+
52+
53+
def invalid_requested_type(type_description: str, requested_type: type) -> TypeError:
54+
"""Create a TypeError for an invalid requested type."""
55+
raise TypeError(
56+
f"The requested type must be {_a(type_description)} type.\n\n"
57+
f"Requested type: {requested_type}"
58+
)
59+
60+
61+
def unsupported_arg(arg_description: str, value: object) -> ValueError:
62+
"""Create a ValueError for an unsupported argument."""
63+
raise ValueError(
64+
f"The {arg_description} argument is not supported.\n\n"
65+
f"Provided value: {reprlib.repr(value)}"
66+
)
67+
68+
69+
# English-specific hack. This is why we prefer "Key: value" for localizable errors. TODO: consider
70+
# moving the full strings into a string table instead of building them out of English noun phrases.
71+
def _a(noun: str) -> str:
72+
indefinite_article = "an" if noun[0] in "AEIOUaeiou" else "a"
73+
if noun.startswith("one-"):
74+
indefinite_article = "a"
75+
return f"{indefinite_article} {noun}"

src/nitypes/time/_conversion.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import hightime as ht
1010

11+
from nitypes._exceptions import invalid_arg_type, invalid_requested_type
12+
1113
if sys.version_info >= (3, 10):
1214
from typing import TypeAlias
1315
else:
@@ -24,15 +26,13 @@ def convert_datetime(requested_type: type[_TDateTime], value: _AnyDateTime, /) -
2426
"""Convert a datetime object to the specified type."""
2527
convert_func = _CONVERT_DATETIME_FOR_TYPE.get(requested_type)
2628
if convert_func is None:
27-
raise TypeError(
28-
"The requested type must be a datetime type.\n\n" f"Requested type: {requested_type}"
29-
)
29+
raise invalid_requested_type("datetime", requested_type)
3030
return cast(_TDateTime, convert_func(value))
3131

3232

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

3737

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

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

6262

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

9896

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

103101

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

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

118116

119117
@_convert_to_ht_timedelta.register

src/nitypes/waveform/_analog_waveform.py

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import numpy.typing as npt
99

1010
from nitypes._arguments import arg_to_uint, validate_dtype
11+
from nitypes._exceptions import invalid_arg_type, invalid_array_ndim
1112
from nitypes.waveform._extended_properties import (
1213
CHANNEL_NAME,
1314
UNIT_DESCRIPTION,
@@ -122,18 +123,16 @@ def from_array_1d(
122123
"""
123124
if isinstance(array, np.ndarray):
124125
if array.ndim != 1:
125-
raise ValueError(
126-
f"The input array must be a one-dimensional array or sequence.\n\nNumber of dimensions: {array.ndim}"
126+
raise invalid_array_ndim(
127+
"input array", "one-dimensional array or sequence", array.ndim
127128
)
128129
elif isinstance(array, Sequence) or (
129130
sys.version_info < (3, 10) and isinstance(array, std_array.array)
130131
):
131132
if dtype is None:
132133
raise ValueError("You must specify a dtype when the input array is a sequence.")
133134
else:
134-
raise TypeError(
135-
f"The input array must be a one-dimensional array or sequence.\n\nType: {type(array)}"
136-
)
135+
raise invalid_arg_type("input array", "one-dimensional array or sequence", array)
137136

138137
return AnalogWaveform(
139138
_data=np.asarray(array, dtype, copy=copy),
@@ -198,18 +197,16 @@ def from_array_2d(
198197
"""
199198
if isinstance(array, np.ndarray):
200199
if array.ndim != 2:
201-
raise ValueError(
202-
f"The input array must be a two-dimensional array or nested sequence.\n\nNumber of dimensions: {array.ndim}"
200+
raise invalid_array_ndim(
201+
"input array", "two-dimensional array or nested sequence", array.ndim
203202
)
204203
elif isinstance(array, Sequence) or (
205204
sys.version_info < (3, 10) and isinstance(array, std_array.array)
206205
):
207206
if dtype is None:
208207
raise ValueError("You must specify a dtype when the input array is a sequence.")
209208
else:
210-
raise TypeError(
211-
f"The input array must be a two-dimensional array or nested sequence.\n\nType: {type(array)}"
212-
)
209+
raise invalid_arg_type("input array", "two-dimensional array or nested sequence", array)
213210

214211
return [
215212
AnalogWaveform(
@@ -367,16 +364,18 @@ def _init_with_provided_array(
367364
capacity: SupportsIndex | None = None,
368365
) -> None:
369366
if not isinstance(data, np.ndarray):
370-
raise TypeError("The input array must be a one-dimensional array.")
367+
raise invalid_arg_type("input array", "one-dimensional array", data)
371368
if data.ndim != 1:
372-
raise ValueError("The input array must be a one-dimensional array.")
369+
raise invalid_array_ndim("input array", "one-dimensional array", data.ndim)
373370

374371
if dtype is None:
375372
dtype = data.dtype
376373
if dtype != data.dtype:
377-
# from_array_1d() converts the input array to the requested dtype, so this error may be
378-
# unreachable.
379-
raise TypeError("The data type of the input array must match the requested data type.")
374+
raise TypeError(
375+
"The data type of the input array must match the requested data type.\n\n"
376+
f"Array data type: {data.dtype}\n"
377+
f"Requested data type: {np.dtype(dtype)}"
378+
)
380379
validate_dtype(dtype, _ANALOG_DTYPES)
381380

382381
capacity = arg_to_uint("capacity", capacity, len(data))
@@ -528,10 +527,7 @@ def capacity(self) -> int:
528527

529528
@capacity.setter
530529
def capacity(self, value: int) -> None:
531-
if value < 0:
532-
raise ValueError(
533-
"The capacity must be a non-negative integer.\n\n" f"Capacity: {value}"
534-
)
530+
value = arg_to_uint("capacity", value)
535531
if value < self._start_index + self._sample_count:
536532
raise ValueError(
537533
"The capacity must be equal to or greater than the number of samples in the waveform.\n\n"
@@ -561,7 +557,7 @@ def channel_name(self) -> str:
561557
@channel_name.setter
562558
def channel_name(self, value: str) -> None:
563559
if not isinstance(value, str):
564-
raise TypeError("The channel name must be a str.\n\n" f"Provided value: {value!r}")
560+
raise invalid_arg_type("channel name", "str", value)
565561
self._extended_properties[CHANNEL_NAME] = value
566562

567563
@property
@@ -574,7 +570,7 @@ def unit_description(self) -> str:
574570
@unit_description.setter
575571
def unit_description(self, value: str) -> None:
576572
if not isinstance(value, str):
577-
raise TypeError("The unit description must be a str.\n\n" f"Provided value: {value!r}")
573+
raise invalid_arg_type("unit description", "str", value)
578574
self._extended_properties[UNIT_DESCRIPTION] = value
579575

580576
@property
@@ -595,7 +591,7 @@ def timing(self) -> Timing:
595591
@timing.setter
596592
def timing(self, value: Timing) -> None:
597593
if not isinstance(value, Timing):
598-
raise TypeError("The timing information must be a Timing object.")
594+
raise invalid_arg_type("timing information", "Timing object", value)
599595
self._timing = value
600596
self._precision_timing = None
601597

@@ -632,7 +628,7 @@ def precision_timing(self) -> PrecisionTiming:
632628
@precision_timing.setter
633629
def precision_timing(self, value: PrecisionTiming) -> None:
634630
if not isinstance(value, PrecisionTiming):
635-
raise TypeError("The precision timing information must be a PrecisionTiming object.")
631+
raise invalid_arg_type("precision timing information", "PrecisionTiming object", value)
636632
self._precision_timing = value
637633
self._timing = None
638634

@@ -644,7 +640,5 @@ def scale_mode(self) -> ScaleMode:
644640
@scale_mode.setter
645641
def scale_mode(self, value: ScaleMode) -> None:
646642
if not isinstance(value, ScaleMode):
647-
raise TypeError(
648-
"The scale mode must be a ScaleMode object.\n\n" f"Provided value: {value!r}"
649-
)
643+
raise invalid_arg_type("scale mode", "ScaleMode object", value)
650644
self._scale_mode = value

0 commit comments

Comments
 (0)