Skip to content

Commit ff0be1d

Browse files
authored
Update DataType.validate to attempt cast (#182)
1 parent dea0464 commit ff0be1d

File tree

4 files changed

+60
-33
lines changed

4 files changed

+60
-33
lines changed

src/fastcs/datatypes.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from collections.abc import Awaitable, Callable
66
from dataclasses import dataclass
77
from functools import cached_property
8-
from typing import Generic, TypeVar
8+
from typing import Any, Generic, TypeVar
99

1010
import numpy as np
1111
from numpy.typing import DTypeLike
@@ -36,12 +36,23 @@ class DataType(Generic[T]):
3636
def dtype(self) -> type[T]: # Using property due to lack of Generic ClassVars
3737
pass
3838

39-
def validate(self, value: T) -> T:
40-
"""Validate a value against fields in the datatype."""
41-
if not isinstance(value, self.dtype):
42-
raise ValueError(f"Value '{value}' is not of type {self.dtype}")
39+
def validate(self, value: Any) -> T:
40+
"""Validate a value against the datatype.
4341
44-
return value
42+
The base implementation is to try the cast and raise a useful error if it fails.
43+
44+
Child classes can implement logic before calling ``super.validate(value)`` to
45+
modify the value passed in and help the cast succeed or after to perform further
46+
validation of the coerced type.
47+
48+
"""
49+
if isinstance(value, self.dtype):
50+
return value
51+
52+
try:
53+
return self.dtype(value)
54+
except (ValueError, TypeError) as e:
55+
raise ValueError(f"Failed to cast {value} to type {self.dtype}") from e
4556

4657
@property
4758
@abstractmethod

tests/test_attribute.py

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
from functools import partial
22

3-
import numpy as np
43
import pytest
54
from pytest_mock import MockerFixture
65

76
from fastcs.attributes import AttrHandlerR, AttrHandlerRW, AttrR, AttrRW, AttrW
8-
from fastcs.datatypes import Enum, Float, Int, String, Waveform
7+
from fastcs.datatypes import Int, String
98

109

1110
@pytest.mark.asyncio
@@ -87,22 +86,3 @@ async def test_handler_initialise(mocker: MockerFixture):
8786

8887
# Assert no error in calling initialise on the TestUpdater handler
8988
await attr.initialise(mocker.ANY)
90-
91-
92-
@pytest.mark.parametrize(
93-
["datatype", "init_args", "value"],
94-
[
95-
(Int, {"min": 1}, 0),
96-
(Int, {"max": -1}, 0),
97-
(Float, {"min": 1}, 0.0),
98-
(Float, {"max": -1}, 0.0),
99-
(Float, {}, 0),
100-
(String, {}, 0),
101-
(Enum, {"enum_cls": int}, 0),
102-
(Waveform, {"array_dtype": "U64", "shape": (1,)}, np.ndarray([1])),
103-
(Waveform, {"array_dtype": "float64", "shape": (1, 1)}, np.ndarray([1])),
104-
],
105-
)
106-
def test_validate(datatype, init_args, value):
107-
with pytest.raises(ValueError):
108-
datatype(**init_args).validate(value)

tests/test_datatypes.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from enum import IntEnum
2+
3+
import numpy as np
4+
import pytest
5+
6+
from fastcs.datatypes import DataType, Enum, Float, Int, Waveform
7+
8+
9+
def test_base_validate():
10+
class TestInt(DataType[int]):
11+
@property
12+
def dtype(self) -> type[int]:
13+
return int
14+
15+
class MyIntEnum(IntEnum):
16+
A = 0
17+
B = 1
18+
19+
test_int = TestInt()
20+
21+
assert test_int.validate("0") == 0
22+
assert test_int.validate(MyIntEnum.B) == 1
23+
24+
with pytest.raises(ValueError, match="Failed to cast"):
25+
test_int.validate("foo")
26+
27+
28+
@pytest.mark.parametrize(
29+
["datatype", "init_args", "value"],
30+
[
31+
(Int, {"min": 1}, 0),
32+
(Int, {"max": -1}, 0),
33+
(Float, {"min": 1}, 0.0),
34+
(Float, {"max": -1}, 0.0),
35+
(Enum, {"enum_cls": int}, 0),
36+
(Waveform, {"array_dtype": "U64", "shape": (1,)}, np.ndarray([1])),
37+
(Waveform, {"array_dtype": "float64", "shape": (1, 1)}, np.ndarray([1])),
38+
],
39+
)
40+
def test_validate(datatype, init_args, value):
41+
with pytest.raises(ValueError):
42+
datatype(**init_args).validate(value)

tests/transport/epics/ca/test_util.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,8 @@ def test_casting_to_epics(datatype, input, output):
9090
@pytest.mark.parametrize(
9191
"datatype, input",
9292
[
93-
(object(), 0),
9493
# TODO cover Waveform and Table cases
95-
(Enum(ShortEnum), 0), # can't use index
9694
(Enum(ShortEnum), LongEnum.TOO), # wrong enum.Enum class
97-
(Int(), 4.0),
98-
(Float(), 1),
99-
(Bool(), None),
100-
(String(), 10),
10195
],
10296
)
10397
def test_cast_to_epics_validations(datatype, input):

0 commit comments

Comments
 (0)