Skip to content

Commit 9556039

Browse files
committed
added limits to Int and Float
1 parent 0e06e16 commit 9556039

File tree

4 files changed

+77
-18
lines changed

4 files changed

+77
-18
lines changed

src/fastcs/attributes.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from enum import Enum
44
from typing import Any, Generic, Protocol, runtime_checkable
55

6-
from .datatypes import ATTRIBUTE_TYPES, AttrCallback, DataType, T
6+
from .datatypes import ATTRIBUTE_TYPES, AttrCallback, DataType, T, validate_value
77

88

99
class AttrMode(Enum):
@@ -115,7 +115,7 @@ def get(self) -> T:
115115
return self._value
116116

117117
async def set(self, value: T) -> None:
118-
self._value = self._datatype.dtype(value)
118+
self._value = self._datatype.dtype(validate_value(self._datatype, value))
119119

120120
if self._update_callback is not None:
121121
await self._update_callback(self._value)
@@ -202,6 +202,6 @@ def __init__(
202202
)
203203

204204
async def process(self, value: T) -> None:
205-
await self.set(value)
205+
await self.set(validate_value(self._datatype, value))
206206

207207
await super().process(value) # type: ignore

src/fastcs/backends/epics/ioc.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,26 @@ def _get_input_record(pv: str, attribute: AttrR) -> RecordWrapper:
186186
match attribute.datatype:
187187
case Bool(znam, onam):
188188
return builder.boolIn(pv, ZNAM=znam, ONAM=onam, **attribute_fields)
189-
case Int():
190-
return builder.longIn(pv, EGU=attribute.datatype.units, **attribute_fields)
191-
case Float(prec):
189+
case Int(units, min, max, min_alarm, max_alarm):
190+
return builder.longIn(
191+
pv,
192+
EGU=units,
193+
DRVL=min,
194+
DRVH=max,
195+
LOPR=min_alarm,
196+
HOPR=max_alarm,
197+
**attribute_fields,
198+
)
199+
case Float(prec, units, min, max, min_alarm, max_alarm):
192200
return builder.aIn(
193-
pv, EGU=attribute.datatype.units, PREC=prec, **attribute_fields
201+
pv,
202+
PREC=prec,
203+
EGU=units,
204+
DRVL=min,
205+
DRVH=max,
206+
LOPR=min_alarm,
207+
HOPR=max_alarm,
208+
**attribute_fields,
194209
)
195210
case String():
196211
return builder.longStringIn(pv, **attribute_fields)
@@ -255,21 +270,29 @@ def _get_output_record(pv: str, attribute: AttrW, on_update: Callable) -> Any:
255270
always_update=True,
256271
on_update=on_update,
257272
)
258-
case Int(units=units):
273+
case Int(units, min, max, min_alarm, max_alarm):
259274
return builder.longOut(
260275
pv,
261276
always_update=True,
262277
on_update=on_update,
263278
EGU=units,
279+
DRVL=min,
280+
DRVH=max,
281+
LOPR=min_alarm,
282+
HOPR=max_alarm,
264283
**attribute_fields,
265284
)
266-
case Float(prec=prec, units=units):
285+
case Float(prec, units, min, max, min_alarm, max_alarm):
267286
return builder.aOut(
268287
pv,
269288
always_update=True,
270289
on_update=on_update,
271-
EGU=units,
272290
PREC=prec,
291+
EGU=units,
292+
DRVL=min,
293+
DRVH=max,
294+
LOPR=min_alarm,
295+
HOPR=max_alarm,
273296
**attribute_fields,
274297
)
275298
case String():

src/fastcs/datatypes.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ class Int(DataType[int]):
2626
"""`DataType` mapping to builtin ``int``."""
2727

2828
units: str | None = None
29+
min: int | None = None
30+
max: int | None = None
31+
min_alarm: int | None = None
32+
max_alarm: int | None = None
2933

3034
@property
3135
def dtype(self) -> type[int]:
@@ -38,6 +42,10 @@ class Float(DataType[float]):
3842

3943
prec: int = 2
4044
units: str | None = None
45+
min: float | None = None
46+
max: float | None = None
47+
min_alarm: float | None = None
48+
max_alarm: float | None = None
4149

4250
@property
4351
def dtype(self) -> type[float]:
@@ -63,3 +71,15 @@ class String(DataType[str]):
6371
@property
6472
def dtype(self) -> type[str]:
6573
return str
74+
75+
76+
def validate_value(datatype: DataType[T], value: T) -> T:
77+
"""Validate a value against a datatype."""
78+
79+
if isinstance(datatype, (Int | Float)):
80+
assert isinstance(value, (int | float)), f"Value {value} is not a number"
81+
if datatype.min is not None and value < datatype.min:
82+
raise ValueError(f"Value {value} is less than minimum {datatype.min}")
83+
if datatype.max is not None and value > datatype.max:
84+
raise ValueError(f"Value {value} is greater than maximum {datatype.max}")
85+
return value

tests/backends/epics/test_ioc.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,15 @@ def test_get_output_record_raises(mocker: MockerFixture):
211211
_get_output_record("PV", mocker.MagicMock(), on_update=mocker.MagicMock())
212212

213213

214+
DEFAULT_SCALAR_FIELD_ARGS = {
215+
"EGU": None,
216+
"DRVL": None,
217+
"DRVH": None,
218+
"LOPR": None,
219+
"HOPR": None,
220+
}
221+
222+
214223
def test_ioc(mocker: MockerFixture, mapping: Mapping):
215224
builder = mocker.patch("fastcs.backends.epics.ioc.builder")
216225
add_pvi_info = mocker.patch("fastcs.backends.epics.ioc._add_pvi_info")
@@ -222,21 +231,26 @@ def test_ioc(mocker: MockerFixture, mapping: Mapping):
222231

223232
# Check records are created
224233
builder.boolIn.assert_called_once_with(f"{DEVICE}:ReadBool", ZNAM="OFF", ONAM="ON")
225-
builder.longIn.assert_any_call(f"{DEVICE}:ReadInt", EGU=None)
234+
builder.longIn.assert_any_call(f"{DEVICE}:ReadInt", **DEFAULT_SCALAR_FIELD_ARGS)
226235
builder.aIn.assert_called_once_with(
227-
f"{DEVICE}:ReadWriteFloat_RBV", PREC=2, EGU=None
236+
f"{DEVICE}:ReadWriteFloat_RBV", PREC=2, **DEFAULT_SCALAR_FIELD_ARGS
228237
)
229238
builder.aOut.assert_any_call(
230239
f"{DEVICE}:ReadWriteFloat",
231240
always_update=True,
232241
on_update=mocker.ANY,
233242
PREC=2,
234-
EGU=None,
243+
**DEFAULT_SCALAR_FIELD_ARGS,
244+
)
245+
builder.longIn.assert_any_call(f"{DEVICE}:BigEnum", **DEFAULT_SCALAR_FIELD_ARGS)
246+
builder.longIn.assert_any_call(
247+
f"{DEVICE}:ReadWriteInt_RBV", **DEFAULT_SCALAR_FIELD_ARGS
235248
)
236-
builder.longIn.assert_any_call(f"{DEVICE}:BigEnum", EGU=None)
237-
builder.longIn.assert_any_call(f"{DEVICE}:ReadWriteInt_RBV", EGU=None)
238249
builder.longOut.assert_called_with(
239-
f"{DEVICE}:ReadWriteInt", always_update=True, on_update=mocker.ANY, EGU=None
250+
f"{DEVICE}:ReadWriteInt",
251+
always_update=True,
252+
on_update=mocker.ANY,
253+
**DEFAULT_SCALAR_FIELD_ARGS,
240254
)
241255
builder.mbbIn.assert_called_once_with(
242256
f"{DEVICE}:StringEnum_RBV", ZRST="red", ONST="green", TWST="blue"
@@ -396,9 +410,11 @@ def test_long_pv_names_discarded(mocker: MockerFixture):
396410
f"{DEVICE}:{short_pv_name}",
397411
always_update=True,
398412
on_update=mocker.ANY,
399-
EGU=None,
413+
**DEFAULT_SCALAR_FIELD_ARGS,
414+
)
415+
builder.longIn.assert_called_once_with(
416+
f"{DEVICE}:{short_pv_name}_RBV", **DEFAULT_SCALAR_FIELD_ARGS
400417
)
401-
builder.longIn.assert_called_once_with(f"{DEVICE}:{short_pv_name}_RBV", EGU=None)
402418

403419
long_pv_name = long_attr_name.title().replace("_", "")
404420
with pytest.raises(AssertionError):

0 commit comments

Comments
 (0)