Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions src/fastcs/attributes/attr_r.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ def __init__(
)
self._update_callback: AttrIOUpdateCallback[DType_T] | None = None
"""Callback to update the value of the attribute with an IO to the source"""
self._on_update_callbacks: list[AttrOnUpdateCallback[DType_T]] | None = None
self._on_update_callbacks: (
list[tuple[AttrOnUpdateCallback[DType_T], bool]] | None
) = None
"""Callbacks to publish changes to the value of the attribute"""
self._on_update_events: set[PredicateEvent[DType_T]] = set()
"""Events to set when the value satisifies some predicate"""
Expand Down Expand Up @@ -75,32 +77,38 @@ async def update(self, value: Any) -> None:
"Attribute set", value=value, value_type=type(value), attribute=self
)

_previous_value = self._value
self._value = self._datatype.validate(value)

self._on_update_events -= {
e for e in self._on_update_events if e.set(self._value)
}

if self._on_update_callbacks is not None:
callbacks_to_call: list[AttrOnUpdateCallback[DType_T]] = [
cb
for cb, always in self._on_update_callbacks
if always or not self.datatype.equal(self._value, _previous_value)
]
try:
await asyncio.gather(
*[cb(self._value) for cb in self._on_update_callbacks]
)
await asyncio.gather(*[cb(self._value) for cb in callbacks_to_call])
except Exception as e:
logger.opt(exception=e).error(
"On update callbacks failed", attribute=self, value=value
)
raise

def add_on_update_callback(self, callback: AttrOnUpdateCallback[DType_T]) -> None:
def add_on_update_callback(
self, callback: AttrOnUpdateCallback[DType_T], always: bool = False
) -> None:
"""Add a callback to be called when the value of the attribute is updated

The callback will be called with the updated value.

"""
if self._on_update_callbacks is None:
self._on_update_callbacks = []
self._on_update_callbacks.append(callback)
self._on_update_callbacks.append((callback, always))

def set_update_callback(self, callback: AttrIOUpdateCallback[DType_T]):
"""Set the callback to update the value of the attribute from the source
Expand Down
10 changes: 8 additions & 2 deletions tests/test_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,11 @@ async def update(attr: AttrR):
@pytest.mark.asyncio
async def test_attributes():
device = {"state": "Idle", "number": 1, "count": False}
ui = {"state": "", "number": 0, "count": False}
ui = {"state": "", "number": 0, "count": False, "update_count": 0}

async def update_ui(value, key):
ui[key] = value
ui["update_count"] += 1

async def send(_attr, value, key):
device[key] = value
Expand All @@ -116,9 +117,14 @@ async def device_add():
device["number"] += 1

attr_r = AttrR(String())
attr_r.add_on_update_callback(partial(update_ui, key="state"))
attr_r.add_on_update_callback(partial(update_ui, key="state"), always=False)
await attr_r.update(device["state"])
assert ui["state"] == "Idle"
# Update with new value triggers callback
assert ui["update_count"] == 1
await attr_r.update(device["state"])
# Identical update does not trigger callback as always=False
assert ui["update_count"] == 1

attr_rw = AttrRW(Int())
attr_rw._on_put_callback = partial(send, key="number")
Expand Down
5 changes: 3 additions & 2 deletions tests/transports/epics/pva/test_p4p.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,9 @@ async def _wait_and_set_attr_r():
await controller.a.update(40_000)
await controller.b.update(-0.99)
await asyncio.sleep(0.05)
await controller.a.update(-100)
await controller.b.update(-0.99)
# Identical value, so will not cause a readback update
await controller.a.update(-100)
await controller.b.update(-0.9111111)

a_values, b_values = [], []
Expand All @@ -253,7 +254,7 @@ async def _wait_and_set_attr_r():
serve.cancel()
wait_and_set_attr_r.cancel()
assert a_values == [0, 40_000, -100]
assert b_values == [0.0, -0.99, -0.99, -0.91] # Last is -0.91 because of prec
assert b_values == [0.0, -0.99, -0.91] # Last is -0.91 because of prec


def test_pvi_grouping():
Expand Down