From ba3900f62d2f31ce1a01282bd0b07876a4efef9f Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Thu, 22 Jan 2026 16:12:38 +0000 Subject: [PATCH 1/4] feat: add always flag to add_on_update_callback --- src/fastcs/attributes/attr_r.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/fastcs/attributes/attr_r.py b/src/fastcs/attributes/attr_r.py index aec7f522..8812dc94 100644 --- a/src/fastcs/attributes/attr_r.py +++ b/src/fastcs/attributes/attr_r.py @@ -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""" @@ -71,6 +73,7 @@ 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 -= { @@ -78,17 +81,22 @@ async def update(self, value: Any) -> None: } 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 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. @@ -96,7 +104,7 @@ def add_on_update_callback(self, callback: AttrOnUpdateCallback[DType_T]) -> Non """ 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 From cb34952f7d6ea9da9824d320511f1d1bad9c8ef6 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Fri, 23 Jan 2026 11:11:15 +0000 Subject: [PATCH 2/4] refactor: handle array equality check in attr_r update --- src/fastcs/attributes/attr_r.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/fastcs/attributes/attr_r.py b/src/fastcs/attributes/attr_r.py index 8812dc94..e37e7714 100644 --- a/src/fastcs/attributes/attr_r.py +++ b/src/fastcs/attributes/attr_r.py @@ -4,6 +4,8 @@ from collections.abc import Awaitable, Callable from typing import Any +import numpy as np + from fastcs.attributes.attribute import Attribute from fastcs.attributes.attribute_io_ref import AttributeIORefT from fastcs.attributes.util import AttrValuePredicate, PredicateEvent @@ -81,11 +83,16 @@ async def update(self, value: Any) -> None: } if self._on_update_callbacks is not None: + if isinstance(self._value, np.ndarray): + assert isinstance(_previous_value, np.ndarray) + _is_changed = not np.array_equal(self._value, _previous_value) + else: + _is_changed = self._value != _previous_value + callbacks_to_call: list[AttrOnUpdateCallback[DType_T]] = [ - cb - for cb, always in self._on_update_callbacks - if always or self._value != _previous_value + cb for cb, always in self._on_update_callbacks if always or _is_changed ] + try: await asyncio.gather(*[cb(self._value) for cb in callbacks_to_call]) except Exception as e: From 52217b820a03d85c6fa3a855fb79c951104c8687 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Fri, 23 Jan 2026 11:11:52 +0000 Subject: [PATCH 3/4] tests: remove duplicate attr_r readback update assertion from test --- tests/transports/epics/pva/test_p4p.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/transports/epics/pva/test_p4p.py b/tests/transports/epics/pva/test_p4p.py index 6c013c6e..3a1a06ac 100644 --- a/tests/transports/epics/pva/test_p4p.py +++ b/tests/transports/epics/pva/test_p4p.py @@ -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 = [], [] @@ -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(): From 470d6b2911b836f71620580f6ba1c30bfebfda66 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Fri, 23 Jan 2026 11:15:22 +0000 Subject: [PATCH 4/4] refactor: use datatype.equal to check equality --- src/fastcs/attributes/attr_r.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/fastcs/attributes/attr_r.py b/src/fastcs/attributes/attr_r.py index e37e7714..788aeefe 100644 --- a/src/fastcs/attributes/attr_r.py +++ b/src/fastcs/attributes/attr_r.py @@ -4,8 +4,6 @@ from collections.abc import Awaitable, Callable from typing import Any -import numpy as np - from fastcs.attributes.attribute import Attribute from fastcs.attributes.attribute_io_ref import AttributeIORefT from fastcs.attributes.util import AttrValuePredicate, PredicateEvent @@ -83,16 +81,11 @@ async def update(self, value: Any) -> None: } if self._on_update_callbacks is not None: - if isinstance(self._value, np.ndarray): - assert isinstance(_previous_value, np.ndarray) - _is_changed = not np.array_equal(self._value, _previous_value) - else: - _is_changed = self._value != _previous_value - callbacks_to_call: list[AttrOnUpdateCallback[DType_T]] = [ - cb for cb, always in self._on_update_callbacks if always or _is_changed + 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 callbacks_to_call]) except Exception as e: