Skip to content

Commit c97ed24

Browse files
committed
Use single autosave kwarg instead of autosave and autosave_fields
1 parent e1e16f7 commit c97ed24

File tree

7 files changed

+103
-90
lines changed

7 files changed

+103
-90
lines changed

docs/examples/example_autosave_ioc.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
# Create records, set some of them to autosave, also save some of their fields
88

99
builder.aOut("AO", autosave=True)
10-
builder.aIn("AI", autosave_fields=["PREC", "EGU"])
10+
builder.aIn("AI", autosave=["PREC", "EGU"])
1111
builder.boolIn("BO")
1212
builder.WaveformIn("WAVEFORMIN", [0, 0, 0, 0], autosave=True)
13-
with autosave.Autosave(True, ["LOPR", "HOPR"]):
14-
builder.aOut("AUTOMATIC-AO", autosave_fields=["EGU"])
13+
with autosave.Autosave(["VAL", "LOPR", "HOPR"]):
14+
builder.aOut("AUTOMATIC-AO", autosave=["EGU"])
1515
seconds = builder.longOut("SECONDSRUN", autosave=True)
1616

1717
autosave.configure(

docs/how-to/use-autosave-in-an-ioc.rst

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ Example IOC
1010
.. literalinclude:: ../examples/example_autosave_ioc.py
1111

1212
Records are instantiated as normal and configured for automatic loading and
13-
periodic saving to a backup file with the keyword arguments ``autosave`` and ``autosave_fields``.
14-
Records with ``autosave=True`` (``False`` by default) have their
15-
VAL fields backed up. Additional record fields in a list passed to ``autosave_fields`` will be backed
16-
up - note that this applies even when ``autosave`` is ``False``.
13+
periodic saving to a backup file with use of the keyword argument ``autosave``.
14+
``autosave`` resolves to a list of strings, which are the names of fields to be
15+
tracked by autosave. By default ``autosave=False``, which disables autosave for that PV.
16+
Setting ``autosave=True`` is equivalent to passing ``["VAL"]``. Note that ``"VAL"`` must be
17+
explicitly passed when tracking other fields, e.g. ``["VAL", "LOPR", "HOPR"]``.
18+
``autosave`` can also accept a single string field name as an argument.
1719

1820
The field values get written into a yaml-formatted file containing key-value pairs.
1921
By default the keys are the same as the full PV name, including any device name specified
@@ -26,10 +28,11 @@ set to 30.0 by default. The directory must exist, and should be configured with
2628
read/write permissions for the user running the IOC.
2729

2830
IOC developers should only need to interface with autosave via the :func:`~softioc.autosave.configure()`
29-
method and the ``autosave`` and ``autosave_fields`` keyword arguments. Alternatively,
31+
method and the ``autosave`` keyword argument. Alternatively,
3032
PVs can be instantiated inside the :class:`~softioc.autosave.Autosave()` context manager, which
31-
automatically passes the arguments ``autosave`` and ``autosave_fields`` to any PVs created
32-
inside the context manager. If the PV already has ``autosave_fields`` set, the lists
33+
automatically passes the ``autosave`` argument to any PVs created
34+
inside the context manager. If any fields are already specified by the ``autosave`` keyword
35+
argument of PV's initialisation call the lists
3336
of fields get combined. All other module members are intended for internal use only.
3437

3538
In normal operation, loading from a backup is performed once during the
@@ -44,7 +47,7 @@ the backup file.
4447
If autosave is enabled and active, a timestamped copy of the latest existing autosave backup file is created
4548
when the IOC is restarted, e.g. ``<name>.softsav_240717-095004`` (timestamps are in the format yymmdd-HHMMSS).
4649
If you only wish to store one backup of the autosave file at a time, ``timestamped_backups=False``
47-
can be passed to :func:`~softioc.autosave.configure()`, this will create a backup file
50+
can be passed to :func:`~softioc.autosave.configure()` when it is called, this will create a backup file
4851
named ``<name>.softsav.bu``. To disable any autosaving, comment out the
4952
:func:`~softioc.autosave.configure()` call or pass it the keyword argument
5053
``enabled=False``.

docs/reference/api.rst

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -196,21 +196,21 @@ and stderr streams, is sent directly to the terminal.
196196
at IOC startup, and no values with be saved to any backup files.
197197

198198
.. seealso::
199-
`softioc.builder` for how to designate a field for autosave.
200199

201-
:ref:`autosave`, the keyword argument used to initialise PVs with the VAL field tracked by autosave.
200+
:ref:`autosave`, the builder keyword argument used to designate PV fields for autosave
202201

203-
`autosave_fields`, the keyword argument used to initialise PVs with designated fields tracked by autosave.
202+
:class:`Autosave` for how to add fields to autosave inside a context manager.
204203

205204
.. class:: Autosave
206205

207-
.. method:: __init__(autosave=True, autosave_fields=None)
206+
.. method:: __init__(autosave=True)
208207

209208
To be called as a context manager. Any PVs that are created inside
210-
the context manager have the arguments ``autosave`` and ``autosave_fields``
211-
passed to them automatically, where ``autosave_fields`` is an optional list of
212-
field names. If the PV already has autosave_fields set, the lists of fields get
213-
combined.
209+
the context manager have the fields passed to the ``autosave`` argument of
210+
the context manager added to autosave tracking. The options for ``autosave``
211+
are identical to the ones described in the builder keyword argument
212+
:ref:`autosave`. If a PV already has :ref:`autosave` set, the two lists of fields
213+
get combined into a single set.
214214

215215

216216

@@ -337,24 +337,24 @@ and stderr streams, is sent directly to the terminal.
337337
:ref:`autosave`
338338
~~~~~~~~~~~~~~~
339339

340-
Available on all record types. When set to `True` it marks the record
341-
value for automatic periodic backing up to a file. Set to `False` by
342-
default. When the IOC is restarted and a backup file exists, the value is
340+
Available on all record types.
341+
Resoles to a list of string field names, when not empty it marks the record
342+
fields for automatic periodic backing up to a file. Set to `False` by
343+
default. When the IOC is restarted and a backup file exists, the saved values are
343344
loaded from this file when :func:`~softioc.builder.LoadDatabase` is called.
344-
The saved value takes priority over any value
345-
given in `initial_value`. No backing up will occur unless autosave is
345+
The saved values takes priority over any initial field value passed to the PV
346+
in `initial_value` or ``**fields``. No backing up will occur unless autosave is
346347
enabled and configured with :func:`~softioc.autosave.configure`.
347348

348-
.. seealso::
349-
:func:`~softioc.autosave.configure` for discussion on how to configure saving.
350-
351-
.. _autosave_fields:
349+
The options for the argument are:
352350

353-
`autosave_fields`
354-
~~~~~~~~~~~~~~~~~
351+
* ``True``, which is equivalent to ``["VAL"]``
352+
* ``False``, which is equivalent to ``[]`` and disables autosave tracking for the PV
353+
* A list of field names such as ``["VAL", "LOPR", "HOPR"]``, note that ``"VAL"`` must be explicitly provided
354+
* A single field name such as ``"EGU"`` which is equivalent to passing ``["EGU"]``.
355355

356-
A list of strings of record fields belonging to the PV (e.g. ["EGU", "PREC"])
357-
to be saved to and loaded from a backup file. Empty by default.
356+
.. seealso::
357+
:func:`~softioc.autosave.configure` for discussion on how to configure saving.
358358

359359

360360
For all of these functions any EPICS database field can be assigned a value by

softioc/autosave.py

Lines changed: 52 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -80,49 +80,50 @@ def _shutdown_autosave_thread(worker):
8080
worker.join()
8181

8282

83-
def add_pv_to_autosave(pv, name, kargs):
83+
def _parse_autosave_fields(fields):
84+
if fields is False:
85+
return []
86+
elif fields is True:
87+
return ["VAL"]
88+
elif isinstance(fields, list):
89+
return fields
90+
elif isinstance(fields, str):
91+
return [fields]
92+
else:
93+
raise ValueError(f"Could not parse autosave fields argument: {fields}")
94+
95+
96+
def add_pv_to_autosave(pv, name, fields):
8497
"""Configures a PV for autosave
8598
8699
Args:
87100
pv: a PV object inheriting ProcessDeviceSupportCore
88101
name: the key by which the PV value is saved to and loaded from a
89102
backup. This is typically the same as the PV name.
90-
kargs: a dictionary containing the optional keys "autosave", a boolean
91-
used to add the VAL field to autosave backups, and
92-
"autosave_fields", a list of string field names to save to the
93-
backup file.
103+
fields: used to determine which fields of a PV are tracked by autosave.
104+
The allowed options are a single string such as "VAL" or "EGU",
105+
a list of strings such as ["VAL", "EGU"], a boolean True which
106+
evaluates to ["VAL"] or False to track no fields. If the PV is
107+
created inside an Autosave context manager, the fields passed to the
108+
context manager are also tracked by autosave.
94109
"""
95-
96110
context = _AutosaveContext()
111+
fields = set(_parse_autosave_fields(fields))
97112
# instantiate to get thread local class variables via instance
98-
if context._in_cm:
99-
# non-None autosave argument to PV takes priority over context manager
100-
autosave_karg = kargs.pop("autosave", None)
101-
save_val = (
102-
autosave_karg
103-
if autosave_karg is not None
104-
else context._val
105-
)
106-
save_fields = (
107-
kargs.pop("autosave_fields", []) + context._fields
108-
)
109-
else:
110-
save_val = kargs.pop("autosave", False)
111-
save_fields = kargs.pop("autosave_fields", [])
112-
if save_val:
113-
Autosave._pvs[name] = _AutosavePV(pv)
114-
if save_fields:
115-
for field in save_fields:
116-
Autosave._pvs[f"{name}.{field}"] = _AutosavePV(pv, field)
113+
if context._in_cm: # _fields should always be a list if in context manager
114+
fields.update(context._fields)
115+
for field in fields:
116+
field_name = name if field == "VAL" else f"{name}.{field}"
117+
Autosave._pvs[field_name] = _AutosavePV(pv, field)
117118

118119

119120
def load_autosave():
120121
Autosave._load()
121122

122123

123124
class _AutosavePV:
124-
def __init__(self, pv, field=None):
125-
if not field or field == "VAL":
125+
def __init__(self, pv, field):
126+
if field == "VAL":
126127
self.get = pv.get
127128
self.set = pv.set
128129
else:
@@ -153,45 +154,55 @@ def _get_backup_sav_path():
153154
sav_path = _get_current_sav_path()
154155
return sav_path.parent / (sav_path.name + ".bu")
155156

157+
156158
class _AutosaveContext(threading.local):
157159
_instance = None
158160
_lock = threading.Lock()
159-
_val = None
160161
_fields = None
161162
_in_cm = False
162-
def __new__(cls, val=None, fields=None):
163+
164+
def __new__(cls, fields=None):
163165
if cls._instance is None:
164166
with cls._lock:
165167
if not cls._instance:
166168
cls._instance = super().__new__(cls)
167-
if cls._instance._in_cm:
168-
if val is not None:
169-
cls._instance._val = val
170-
if fields is not None:
171-
cls._instance._fields = fields or []
169+
if cls._instance._in_cm and fields is not None:
170+
cls._instance._fields = fields or []
172171
return cls._instance
173172

174173
def reset(self):
175174
self._fields = None
176-
self._val = None
177175
self._in_cm = False
178176

177+
179178
class Autosave:
180179
_pvs = {}
181180
_last_saved_state = {}
182181
_last_saved_time = datetime.now()
183182
_stop_event = threading.Event()
184183
_loop_started = False
185184

186-
def __init__(self, autosave=None, autosave_fields=None):
185+
def __init__(self, fields=True):
186+
"""
187+
When called as a context manager, any PVs created in the context have
188+
the fields provided by the fields argument added to autosave backups.
189+
190+
Args:
191+
fields: a list of string field names to be periodically saved to a
192+
backup file, which are loaded from on IOC restart.
193+
The allowed options are a single string such as "VAL" or "EGU",
194+
a list of strings such as ["VAL", "EGU"], a boolean True which
195+
evaluates to ["VAL"] or False to track no additional fields.
196+
If the autosave keyword is already specified in a PV's
197+
initialisation, the list of fields to track are combined.
198+
"""
187199
context = _AutosaveContext()
188200
if context._in_cm:
189201
raise RuntimeError(
190-
"Can not instantiate Autosave when already in context manager")
191-
if autosave is not None:
192-
context._val = autosave
193-
if autosave_fields is not None:
194-
context._fields = autosave_fields
202+
"Can not instantiate Autosave when already in context manager"
203+
)
204+
fields = _parse_autosave_fields(fields)
205+
context._fields = fields
195206

196207
def __enter__(self):
197208
context = _AutosaveContext()

softioc/device.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ class ProcessDeviceSupportCore(DeviceSupportCore, RecordLookup):
5555

5656
# all record types can support autosave
5757
def __init__(self, name, **kargs):
58-
autosave.add_pv_to_autosave(self, name, kargs)
58+
autosave_fields = kargs.pop("autosave", False)
59+
autosave.add_pv_to_autosave(self, name, autosave_fields)
5960
self.__super.__init__(name, **kargs)
6061

6162
# Most subclasses (all except waveforms) define a ctypes constructor for the

softioc/pythonSoftIoc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def __init__(self, builder, device, name, **fields):
2525
DeviceKeywords = [
2626
'on_update', 'on_update_name', 'validate', 'always_update',
2727
'initial_value', '_wf_nelm', '_wf_dtype', 'blocking',
28-
'autosave', 'autosave_fields'
28+
'autosave'
2929
]
3030
device_kargs = {}
3131
for keyword in DeviceKeywords:

tests/test_autosave.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ def reset_autosave_setup_teardown():
2020
default_tb = autosave.AutosaveConfig.timestamped_backups
2121
default_pvs = autosave.Autosave._pvs.copy()
2222
default_state = autosave.Autosave._last_saved_state.copy()
23-
default_cm_save_val = autosave._AutosaveContext._val
2423
default_cm_save_fields = autosave._AutosaveContext._fields
2524
default_instance = autosave._AutosaveContext._instance
2625
yield
@@ -32,7 +31,6 @@ def reset_autosave_setup_teardown():
3231
autosave.Autosave._pvs = default_pvs
3332
autosave.Autosave._last_saved_state = default_state
3433
autosave.Autosave._stop_event = threading.Event()
35-
autosave._AutosaveContext._val = default_cm_save_val
3634
autosave._AutosaveContext._fields = default_cm_save_fields
3735
autosave._AutosaveContext._instance = default_instance
3836

@@ -142,8 +140,8 @@ def test_all_record_types_saveable(tmp_path):
142140

143141

144142
def test_can_save_fields(tmp_path):
145-
builder.aOut("SAVEVAL", autosave=True, autosave_fields=["DISA"])
146-
builder.aOut("DONTSAVEVAL", autosave_fields=["SCAN"])
143+
builder.aOut("SAVEVAL", autosave=["VAL", "DISA"])
144+
builder.aOut("DONTSAVEVAL", autosave=["SCAN"])
147145
# we need to patch get_field as we can't call builder.LoadDatabase()
148146
# and softioc.iocInit() in unit tests
149147
with patch(
@@ -219,11 +217,11 @@ def test_autosave_key_names(tmp_path):
219217

220218

221219
def test_context_manager(tmp_path):
222-
builder.aOut("MANUAL", autosave=True, autosave_fields=["EGU"])
223-
with autosave.Autosave(True, ["PINI"]):
220+
builder.aOut("MANUAL", autosave=["VAL", "EGU"])
221+
with autosave.Autosave(["VAL", "PINI"]):
224222
builder.aOut("AUTOMATIC")
225223
builder.aOut(
226-
"AUTOMATIC-OVERRIDDEN", autosave=False, autosave_fields=["SCAN"]
224+
"AUTOMATIC-EXTRA-FIELD", autosave=["SCAN"]
227225
)
228226
autosave.configure(tmp_path, DEVICE_NAME)
229227
with patch(
@@ -237,9 +235,9 @@ def test_context_manager(tmp_path):
237235
assert "MANUAL.EGU" in saved
238236
assert "AUTOMATIC" in saved
239237
assert "AUTOMATIC.PINI" in saved
240-
assert "AUTOMATIC-OVERRIDDEN" not in saved
241-
assert "AUTOMATIC-OVERRIDDEN.SCAN" in saved
242-
assert "AUTOMATIC-OVERRIDDEN.PINI" in saved
238+
assert "AUTOMATIC-EXTRA-FIELD" in saved
239+
assert "AUTOMATIC-EXTRA-FIELD.SCAN" in saved
240+
assert "AUTOMATIC-EXTRA-FIELD.PINI" in saved
243241

244242

245243
def check_all_record_types_load_properly(device_name, autosave_dir, conn):
@@ -401,9 +399,9 @@ def check_autosave_field_names_contain_device_prefix(
401399
device_name, tmp_path, conn
402400
):
403401
autosave.configure(tmp_path, device_name, save_period=1)
404-
builder.aOut("BEFORE", autosave=True, autosave_fields=["EGU"])
402+
builder.aOut("BEFORE", autosave=["VAL", "EGU"])
405403
builder.SetDeviceName(device_name)
406-
builder.aOut("AFTER", autosave=True, autosave_fields=["EGU"])
404+
builder.aOut("AFTER", autosave=["VAL", "EGU"])
407405
builder.LoadDatabase()
408406
softioc.iocInit()
409407
time.sleep(2)
@@ -438,7 +436,7 @@ def create_pv_in_thread(name, wait):
438436
pv_thread_in_cm = threading.Thread(
439437
target=create_pv_in_thread, args=["PV-FROM-THREAD-DURING", 0])
440438
pv_thread_before_cm.start()
441-
with autosave.Autosave(True, ["EGU"]):
439+
with autosave.Autosave(["VAL", "EGU"]):
442440
builder.aOut("PV-FROM-CM")
443441
pv_thread_in_cm.start()
444442
pv_thread_in_cm.join()
@@ -451,9 +449,9 @@ def create_pv_in_thread(name, wait):
451449

452450
def test_nested_context_managers_raises(tmp_path):
453451
autosave.configure(tmp_path, DEVICE_NAME)
454-
with autosave.Autosave(False, ["SCAN"]):
452+
with autosave.Autosave(["SCAN"]):
455453
with pytest.raises(RuntimeError):
456-
with autosave.Autosave(True, []):
454+
with autosave.Autosave(False):
457455
builder.aOut("MY-PV")
458456
with pytest.raises(RuntimeError):
459457
autosave.Autosave()

0 commit comments

Comments
 (0)