Skip to content

Commit a595c17

Browse files
authored
waveform: Implement scaling (#7)
* waveform: Scaling work-in-progress * waveform: Move initialization of ScaleMode.none * waveform: Work around apparent bug in numpy type hints * waveform: Rework float parameter validation * tests: Add test for NoneScaleMode * tests: Fix some type hints * waveform: Add a comment about float(x) accepting strings * waveform: Use repr in TypeError messages * waveform: Add doctests for arg_to_x * waveform: Add AnalogWaveform.scale_mode * waveform: Clean up some error messages * waveform: Implement scaling * tests: Add waveform scaling tests * waveform: Remove ScaleMode.none dependency cycle * waveform: Remove AnalogWaveform <-> ScaleMode dependency cycle * nitypes: Move utils to a more central location * waveform: Clarify get_scaled_data usage
1 parent 55a6723 commit a595c17

File tree

16 files changed

+827
-99
lines changed

16 files changed

+827
-99
lines changed

src/nitypes/_arguments.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
from __future__ import annotations
2+
3+
import operator
4+
from typing import SupportsFloat, SupportsIndex
5+
6+
import numpy as np
7+
import numpy.typing as npt
8+
9+
10+
def arg_to_float(
11+
arg_description: str, value: SupportsFloat | None, default_value: float | None = None
12+
) -> float:
13+
"""Convert an argument to a float.
14+
15+
>>> arg_to_float("xyz", 1.234)
16+
1.234
17+
>>> arg_to_float("xyz", 1234)
18+
1234.0
19+
>>> arg_to_float("xyz", np.float64(1.234))
20+
np.float64(1.234)
21+
>>> arg_to_float("xyz", np.float32(1.234)) # doctest: +ELLIPSIS
22+
1.233999...
23+
>>> arg_to_float("xyz", 1.234, 5.0)
24+
1.234
25+
>>> arg_to_float("xyz", None, 5.0)
26+
5.0
27+
>>> arg_to_float("xyz", None)
28+
Traceback (most recent call last):
29+
...
30+
TypeError: The xyz must be a floating point number.
31+
<BLANKLINE>
32+
Provided value: None
33+
>>> arg_to_float("xyz", "1.234")
34+
Traceback (most recent call last):
35+
...
36+
TypeError: The xyz must be a floating point number.
37+
<BLANKLINE>
38+
Provided value: '1.234'
39+
"""
40+
if value is None:
41+
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+
)
46+
value = default_value
47+
48+
if not isinstance(value, float):
49+
try:
50+
# Use value.__float__() because float(value) also accepts strings.
51+
return value.__float__()
52+
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
57+
58+
return value
59+
60+
61+
def arg_to_int(
62+
arg_description: str, value: SupportsIndex | None, default_value: int | None = None
63+
) -> int:
64+
"""Convert an argument to a signed integer.
65+
66+
>>> arg_to_int("xyz", 1234)
67+
1234
68+
>>> arg_to_int("xyz", 1234, -1)
69+
1234
70+
>>> arg_to_int("xyz", None, -1)
71+
-1
72+
>>> arg_to_int("xyz", None)
73+
Traceback (most recent call last):
74+
...
75+
TypeError: The xyz must be an integer.
76+
<BLANKLINE>
77+
Provided value: None
78+
>>> arg_to_int("xyz", 1.234)
79+
Traceback (most recent call last):
80+
...
81+
TypeError: The xyz must be an integer.
82+
<BLANKLINE>
83+
Provided value: 1.234
84+
>>> arg_to_int("xyz", "1234")
85+
Traceback (most recent call last):
86+
...
87+
TypeError: The xyz must be an integer.
88+
<BLANKLINE>
89+
Provided value: '1234'
90+
"""
91+
if value is None:
92+
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+
)
96+
value = default_value
97+
98+
if not isinstance(value, int):
99+
try:
100+
return operator.index(value)
101+
except Exception:
102+
raise TypeError(
103+
f"The {arg_description} must be an integer.\n\n" f"Provided value: {value!r}"
104+
) from None
105+
106+
return value
107+
108+
109+
def arg_to_uint(
110+
arg_description: str, value: SupportsIndex | None, default_value: int | None = None
111+
) -> int:
112+
"""Convert an argument to an unsigned integer.
113+
114+
>>> arg_to_uint("xyz", 1234)
115+
1234
116+
>>> arg_to_uint("xyz", 1234, 5000)
117+
1234
118+
>>> arg_to_uint("xyz", None, 5000)
119+
5000
120+
>>> arg_to_uint("xyz", -1234)
121+
Traceback (most recent call last):
122+
...
123+
ValueError: The xyz must be a non-negative integer.
124+
<BLANKLINE>
125+
Provided value: -1234
126+
>>> arg_to_uint("xyz", "1234")
127+
Traceback (most recent call last):
128+
...
129+
TypeError: The xyz must be an integer.
130+
<BLANKLINE>
131+
Provided value: '1234'
132+
"""
133+
value = arg_to_int(arg_description, value, default_value)
134+
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+
)
139+
return value
140+
141+
142+
def validate_dtype(dtype: npt.DTypeLike, supported_dtypes: tuple[npt.DTypeLike, ...]) -> None:
143+
"""Validate a dtype-like object against a tuple of supported dtype-like objects.
144+
145+
>>> validate_dtype(np.float64, (np.float64, np.intc, np.long,))
146+
>>> validate_dtype("float64", (np.float64, np.intc, np.long,))
147+
>>> validate_dtype(np.float64, (np.byte, np.short, np.intc, np.int_, np.long, np.longlong))
148+
Traceback (most recent call last):
149+
...
150+
TypeError: The requested data type is not supported.
151+
<BLANKLINE>
152+
Data type: float64
153+
Supported data types: int8, int16, int32, int64
154+
"""
155+
if not isinstance(dtype, (type, np.dtype)):
156+
dtype = np.dtype(dtype)
157+
if not np.isdtype(dtype, supported_dtypes):
158+
# Remove duplicate names because distinct types (e.g. int vs. long) may have the same name
159+
# ("int32").
160+
supported_dtype_names = {np.dtype(d).name: None for d in supported_dtypes}.keys()
161+
raise TypeError(
162+
"The requested data type is not supported.\n\n"
163+
f"Data type: {np.dtype(dtype)}\n"
164+
f"Supported data types: {', '.join(supported_dtype_names)}"
165+
)

src/nitypes/_exceptions.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
5+
6+
def add_note(exception: Exception, note: str) -> None:
7+
"""Add a note to an exception.
8+
9+
>>> try:
10+
... raise ValueError("Oh no")
11+
... except Exception as e:
12+
... add_note(e, "p.s. This is bad")
13+
... raise
14+
Traceback (most recent call last):
15+
...
16+
ValueError: Oh no
17+
p.s. This is bad
18+
"""
19+
if sys.version_info >= (3, 11):
20+
exception.add_note(note)
21+
else:
22+
message = exception.args[0] + "\n" + note
23+
exception.args = (message,) + exception.args[1:]

src/nitypes/time/_conversion.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def convert_datetime(requested_type: type[_TDateTime], value: _AnyDateTime, /) -
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}")
35+
raise TypeError("The value must be a datetime.\n\n" f"Provided value: {value!r}")
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}")
60+
raise TypeError("The value must be a datetime.\n\n" f"Provided value: {value!r}")
6161

6262

6363
@_convert_to_ht_datetime.register
@@ -98,7 +98,7 @@ def convert_timedelta(requested_type: type[_TTimeDelta], value: _AnyTimeDelta, /
9898

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

103103

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

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

118118

119119
@_convert_to_ht_timedelta.register

src/nitypes/waveform/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
ExtendedPropertyDictionary,
66
ExtendedPropertyValue,
77
)
8+
from nitypes.waveform._scaling import (
9+
NO_SCALING,
10+
LinearScaleMode,
11+
NoneScaleMode,
12+
ScaleMode,
13+
)
814
from nitypes.waveform._timing._base import BaseTiming, SampleIntervalMode
915
from nitypes.waveform._timing._precision import PrecisionTiming
1016
from nitypes.waveform._timing._standard import Timing
@@ -14,8 +20,12 @@
1420
"BaseTiming",
1521
"ExtendedPropertyDictionary",
1622
"ExtendedPropertyValue",
23+
"LinearScaleMode",
24+
"NO_SCALING",
25+
"NoneScaleMode",
1726
"PrecisionTiming",
1827
"SampleIntervalMode",
28+
"ScaleMode",
1929
"Timing",
2030
]
2131

@@ -24,6 +34,9 @@
2434
BaseTiming.__module__ = __name__
2535
ExtendedPropertyDictionary.__module__ = __name__
2636
# ExtendedPropertyValue is a TypeAlias
37+
LinearScaleMode.__module__ = __name__
38+
NoneScaleMode.__module__ = __name__
2739
PrecisionTiming.__module__ = __name__
2840
SampleIntervalMode.__module__ = __name__
41+
ScaleMode.__module__ = __name__
2942
Timing.__module__ = __name__

0 commit comments

Comments
 (0)