Skip to content

Commit 3e3d20e

Browse files
Add exception on overlong string setting
Also add more tests
1 parent 3bb01fa commit 3e3d20e

7 files changed

Lines changed: 109 additions & 26 deletions

File tree

‎.vscode/launch.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"type": "python",
1010
"request": "launch",
1111
"program": "${file}",
12-
"console": "integratedTerminal"
12+
"console": "integratedTerminal",
13+
"justMyCode": false,
1314
},
1415
{
1516
"name": "Debug Unit Test",

‎CHANGELOG.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Added:
2626

2727
- `Allow "status" and "severity" on In record init <../../pull/111>`_
2828
- `Allow users to reset the list of created records <../../pull/114>`_
29+
- `Allow arrays of strings to be used with Waveforms <../../pull/102>`_
2930

3031
4.1.0_ - 2022-08-05
3132
-------------------

‎docs/reference/api.rst‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,11 @@ All functions return a wrapped `ProcessDeviceSupportIn` or
342342
field type name. Otherwise the field type is taken from the initial value
343343
if given, or defaults to ``'FLOAT'``.
344344

345+
.. note::
346+
When storing arrays of strings, it is possible to store Unicode characters.
347+
However, as EPICS has no Unicode support the resultant values will be stored
348+
as byte strings. Care must be taken when encoding/decoding the values.
349+
345350

346351
The following functions generates specialised records.
347352

‎softioc/builder.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from . import device, pythonSoftIoc # noqa
1313
# Re-export this so users only have to import the builder
14-
from .device import SetBlocking # noqa
14+
from .device import SetBlocking, to_epics_str_array # noqa
1515

1616
PythonDevice = pythonSoftIoc.PythonDevice()
1717

@@ -148,7 +148,7 @@ def Action(name, **fields):
148148
'uint32': 'ULONG',
149149
'float32': 'FLOAT',
150150
'float64': 'DOUBLE',
151-
'bytes320': 'STRING', # Numpy's term for a 40-character string (40*8 bits)
151+
'bytes320': 'STRING', # Numpy term for 40-character byte str (40*8 bits)
152152
}
153153

154154
# Coverts FTVL string to numpy type
@@ -220,8 +220,8 @@ def _waveform(value, fields):
220220
initial_value = numpy.require(initial_value, numpy.int32)
221221
elif initial_value.dtype == numpy.uint64:
222222
initial_value = numpy.require(initial_value, numpy.uint32)
223-
elif initial_value.dtype.char in ("S", "U"):
224-
initial_value = numpy.require(initial_value, numpy.dtype("S40"))
223+
elif initial_value.dtype.char in ('S', 'U'):
224+
initial_value = to_epics_str_array(initial_value)
225225
else:
226226
initial_value = numpy.array([], dtype = datatype)
227227
length = _get_length(fields)

‎softioc/device.py‎

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -351,20 +351,28 @@ class ao(ProcessDeviceSupportOut):
351351
_ctype_ = c_double
352352
_dbf_type_ = fields.DBF_DOUBLE
353353

354+
def to_epics_str_array(value):
355+
"""Convert the given array of Python strings to an array of EPICS
356+
nul-terminated strings"""
357+
result = numpy.empty(len(value), 'S40')
358+
359+
for n, s in enumerate(value):
360+
if isinstance(s, str):
361+
val = EpicsString._ctype_()
362+
val.value = s.encode() + b'\0'
363+
result[n] = val.value
364+
else:
365+
result[n] = s
366+
return result
367+
354368

355369
def _require_waveform(value, dtype):
356370
if isinstance(value, bytes):
357371
# Special case hack for byte arrays. Surprisingly tricky:
358372
value = numpy.frombuffer(value, dtype = numpy.uint8)
359373

360374
if dtype and dtype.char == 'S':
361-
result = numpy.empty(len(value), 'S40')
362-
for n, s in enumerate(value):
363-
if isinstance(s, str):
364-
result[n] = s.encode('UTF-8')
365-
else:
366-
result[n] = s
367-
return result
375+
return to_epics_str_array(value)
368376

369377
value = numpy.require(value, dtype = dtype)
370378
if value.shape == ():
@@ -419,10 +427,6 @@ def _value_to_epics(self, value):
419427
# common class of bug, at the cost of duplicated code and data, here we
420428
# ensure a copy is taken of the value.
421429
assert len(value) <= self._nelm, 'Value too long for waveform'
422-
# TODO: line below triggers "DeprecationWarning: Applying '+' to a
423-
# non-numerical array is ill-defined. Returning a copy, but in the
424-
# future this will error."
425-
# Probably should change to numpy.copy?
426430
return +value
427431

428432
def _epics_to_value(self, value):

‎softioc/fields.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,5 +146,4 @@ def __set_time(self, address, new_time):
146146
address[0].secs -= EPICS_epoch
147147

148148

149-
# TODO: DbfCodeToNumpy doesn't exist!
150149
__all__ = ['RecordFactory', 'DbfCodeToNumpy', 'ca_timestamp']

‎tests/test_record_values.py‎

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"string and so lets test it and prove that shall we?"
3333

3434
# The numpy dtype of all arrays of strings
35-
DTYPE_STRING = "S40"
35+
NUMPY_DTYPE_STRING = "S40"
3636

3737

3838
def record_func_names(fixture_value):
@@ -194,15 +194,12 @@ def record_values_names(fixture_value):
194194
),
195195
numpy.ndarray,
196196
),
197-
198-
# TODO: Unicode versions of below tests?
199-
200197
(
201198
"wIn_byte_string_array",
202199
builder.WaveformIn,
203200
[b"AB123", b"CD456", b"EF789"],
204201
numpy.array(
205-
["AB123", "CD456", "EF789"], dtype=DTYPE_STRING
202+
["AB123", "CD456", "EF789"], dtype=NUMPY_DTYPE_STRING
206203
),
207204
numpy.ndarray,
208205
),
@@ -211,7 +208,35 @@ def record_values_names(fixture_value):
211208
builder.WaveformOut,
212209
[b"12AB", b"34CD", b"56EF"],
213210
numpy.array(
214-
["12AB", "34CD", "56EF"], dtype=DTYPE_STRING
211+
["12AB", "34CD", "56EF"], dtype=NUMPY_DTYPE_STRING
212+
),
213+
numpy.ndarray,
214+
),
215+
(
216+
"wIn_unicode_string_array",
217+
builder.WaveformIn,
218+
["12€½", "34¾²", "56¹³"],
219+
numpy.array(
220+
[
221+
b'12\xe2\x82\xac\xc2\xbd',
222+
b'34\xc2\xbe\xc2\xb2',
223+
b'56\xc2\xb9\xc2\xb3'
224+
],
225+
dtype=NUMPY_DTYPE_STRING
226+
),
227+
numpy.ndarray,
228+
),
229+
(
230+
"wOut_unicode_string_array",
231+
builder.WaveformOut,
232+
["12€½", "34¾²", "56¹³"],
233+
numpy.array(
234+
[
235+
b'12\xe2\x82\xac\xc2\xbd',
236+
b'34\xc2\xbe\xc2\xb2',
237+
b'56\xc2\xb9\xc2\xb3'
238+
],
239+
dtype=NUMPY_DTYPE_STRING
215240
),
216241
numpy.ndarray,
217242
),
@@ -220,7 +245,7 @@ def record_values_names(fixture_value):
220245
builder.WaveformIn,
221246
["123abc", "456def", "7890ghi"],
222247
numpy.array(
223-
["123abc", "456def", "7890ghi"], dtype=DTYPE_STRING
248+
["123abc", "456def", "7890ghi"], dtype=NUMPY_DTYPE_STRING
224249
),
225250
numpy.ndarray,
226251
),
@@ -229,7 +254,7 @@ def record_values_names(fixture_value):
229254
builder.WaveformOut,
230255
["123abc", "456def", "7890ghi"],
231256
numpy.array(
232-
["123abc", "456def", "7890ghi"], dtype=DTYPE_STRING
257+
["123abc", "456def", "7890ghi"], dtype=NUMPY_DTYPE_STRING
233258
),
234259
numpy.ndarray,
235260
),
@@ -539,10 +564,19 @@ def is_valid(configuration):
539564
"scalar. Therefore we skip this check.")
540565
continue
541566

567+
# caget on a waveform of strings will return unicode. Have to
568+
# convert it manually to binary.
542569
if isinstance(rec_val, numpy.ndarray) and len(rec_val) > 1 \
543570
and rec_val.dtype.char in ["S", "U"]:
571+
result = numpy.empty(len(rec_val), NUMPY_DTYPE_STRING)
572+
for n, s in enumerate(rec_val):
573+
if isinstance(s, str):
574+
result[n] = s.encode('UTF-8', errors= 'ignore')
575+
else:
576+
result[n] = s
577+
rec_val = result
544578
# caget won't retrieve the full length 40 buffer
545-
rec_val = rec_val.astype(DTYPE_STRING)
579+
rec_val = rec_val.astype(NUMPY_DTYPE_STRING)
546580

547581
record_value_asserts(
548582
creation_func, rec_val, expected_value, expected_type
@@ -958,7 +992,46 @@ def test_waveform_rejects_overlong_values(self):
958992
w_in = builder.WaveformIn("W_IN", [1, 2, 3])
959993
w_out = builder.WaveformOut("W_OUT", [1, 2, 3])
960994

995+
w_in_str = builder.WaveformIn("W_IN_STR", ["ABC", "DEF"])
996+
w_out_str = builder.WaveformOut("W_OUT_STR", ["ABC", "DEF"])
997+
961998
with pytest.raises(AssertionError):
962999
w_in.set([1, 2, 3, 4])
9631000
with pytest.raises(AssertionError):
9641001
w_out.set([1, 2, 3, 4])
1002+
with pytest.raises(AssertionError):
1003+
w_in_str.set(["ABC", "DEF", "GHI"])
1004+
with pytest.raises(AssertionError):
1005+
w_out_str.set(["ABC", "DEF", "GHI"])
1006+
1007+
def test_waveform_rejects_late_strings(self):
1008+
"""Test that a waveform won't allow a list of strings to be assigned
1009+
if no list was given in initial waveform construction"""
1010+
w_in = builder.WaveformIn("W_IN", length=10)
1011+
w_out = builder.WaveformOut("W_OUT", length=10)
1012+
1013+
with pytest.raises(ValueError):
1014+
w_in.set(["ABC", "DEF"])
1015+
with pytest.raises(ValueError):
1016+
w_out.set(["ABC", "DEF"])
1017+
1018+
def test_waveform_rejects_long_array_of_strings(self):
1019+
"""Test that a waveform of strings won't accept too long strings"""
1020+
w_in = builder.WaveformIn(
1021+
"W_IN",
1022+
initial_value=["123abc", "456def", "7890ghi"]
1023+
)
1024+
w_out = builder.WaveformIn(
1025+
"W_OUT",
1026+
initial_value=["123abc", "456def", "7890ghi"]
1027+
)
1028+
1029+
with pytest.raises(AssertionError):
1030+
w_in.set(["1", "2", "3", "4"])
1031+
with pytest.raises(AssertionError):
1032+
w_out.set(["1", "2", "3", "4"])
1033+
1034+
with pytest.raises(ValueError):
1035+
w_in.set([VERY_LONG_STRING])
1036+
with pytest.raises(ValueError):
1037+
w_out.set([VERY_LONG_STRING])

0 commit comments

Comments
 (0)