From 1526ecd094654c5b2bbecee2a27787a93fd1ea65 Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Thu, 23 Oct 2025 16:00:11 +0000 Subject: [PATCH] Redesign attribute callbacks --- docs/conf.py | 3 + docs/snippets/dynamic.py | 2 +- docs/snippets/static07.py | 2 +- docs/snippets/static08.py | 2 +- docs/snippets/static09.py | 2 +- docs/snippets/static10.py | 2 +- docs/snippets/static11.py | 2 +- docs/snippets/static12.py | 4 +- docs/snippets/static13.py | 6 +- src/fastcs/attribute_io.py | 4 +- src/fastcs/attribute_io_ref.py | 2 +- src/fastcs/attributes.py | 193 +++++++++++++----- src/fastcs/controller.py | 55 ++--- src/fastcs/cs_methods.py | 2 +- src/fastcs/datatypes.py | 5 - src/fastcs/demo/controllers.py | 6 +- src/fastcs/launch.py | 24 +-- src/fastcs/transport/epics/ca/ioc.py | 14 +- .../transport/epics/pva/_pv_handlers.py | 16 +- src/fastcs/transport/graphql/graphql.py | 2 +- src/fastcs/transport/rest/rest.py | 8 +- src/fastcs/transport/tango/dsr.py | 2 +- tests/assertable_controller.py | 8 +- tests/conftest.py | 10 +- tests/example_p4p_ioc.py | 7 +- tests/test_attribute.py | 91 ++++----- tests/test_launch.py | 6 +- tests/transport/epics/ca/test_softioc.py | 18 +- tests/transport/epics/pva/test_p4p.py | 42 +--- tests/transport/rest/test_rest.py | 12 +- tests/transport/tango/test_dsr.py | 7 +- 31 files changed, 281 insertions(+), 278 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 2b758cfea..0b1ee840f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -88,6 +88,8 @@ ("py:class", "p4p.nt.ndarray.NTNDArray"), ("py:class", "p4p.nt.NTTable"), # Problems in FastCS itself + ("py:class", "T"), + ("py:class", "AttrIOUpdateCallback"), ("py:class", "fastcs.transport.epics.pva.pvi_tree._PviSignalInfo"), ("py:class", "fastcs.logging._logging.LogLevel"), ("py:class", "fastcs.logging._graylog.GraylogEndpoint"), @@ -102,6 +104,7 @@ ] nitpick_ignore_regex = [ ("py:class", "fastcs.*.T"), + ("py:obj", "fastcs.*.T"), (r"py:.*", r"fastcs\.demo.*"), (r"py:.*", r"tickit.*"), ] diff --git a/docs/snippets/dynamic.py b/docs/snippets/dynamic.py index 63714a71b..c02af58fa 100644 --- a/docs/snippets/dynamic.py +++ b/docs/snippets/dynamic.py @@ -79,7 +79,7 @@ async def update(self, attr: AttrR[NumberT, TemperatureControllerAttributeIORef] response = await self._connection.send_query(f"{query}\r\n") value = response.strip("\r\n") - await attr.set(attr.dtype(value)) + await attr.update(attr.dtype(value)) async def send( self, attr: AttrW[NumberT, TemperatureControllerAttributeIORef], value: NumberT diff --git a/docs/snippets/static07.py b/docs/snippets/static07.py index c8836b327..6677367ba 100644 --- a/docs/snippets/static07.py +++ b/docs/snippets/static07.py @@ -30,7 +30,7 @@ async def update(self, attr: AttrR[NumberT, IDAttributeIORef]): response = await self._connection.send_query("ID?\r\n") value = response.strip("\r\n") - await attr.set(attr.dtype(value)) + await attr.update(attr.dtype(value)) class TemperatureController(Controller): diff --git a/docs/snippets/static08.py b/docs/snippets/static08.py index fdc7afc0b..d64729167 100644 --- a/docs/snippets/static08.py +++ b/docs/snippets/static08.py @@ -35,7 +35,7 @@ async def update(self, attr: AttrR[NumberT, TemperatureControllerAttributeIORef] response = await self._connection.send_query(f"{query}\r\n") value = response.strip("\r\n") - await attr.set(attr.dtype(value)) + await attr.update(attr.dtype(value)) class TemperatureController(Controller): diff --git a/docs/snippets/static09.py b/docs/snippets/static09.py index 48936caf0..cf10f6b33 100644 --- a/docs/snippets/static09.py +++ b/docs/snippets/static09.py @@ -35,7 +35,7 @@ async def update(self, attr: AttrR[NumberT, TemperatureControllerAttributeIORef] response = await self._connection.send_query(f"{query}\r\n") value = response.strip("\r\n") - await attr.set(attr.dtype(value)) + await attr.update(attr.dtype(value)) async def send( self, attr: AttrW[NumberT, TemperatureControllerAttributeIORef], value: NumberT diff --git a/docs/snippets/static10.py b/docs/snippets/static10.py index 59014e0c9..85d0ae2b6 100644 --- a/docs/snippets/static10.py +++ b/docs/snippets/static10.py @@ -36,7 +36,7 @@ async def update(self, attr: AttrR[NumberT, TemperatureControllerAttributeIORef] response = await self._connection.send_query(f"{query}\r\n") value = response.strip("\r\n") - await attr.set(attr.dtype(value)) + await attr.update(attr.dtype(value)) async def send( self, attr: AttrW[NumberT, TemperatureControllerAttributeIORef], value: NumberT diff --git a/docs/snippets/static11.py b/docs/snippets/static11.py index 78c537bf3..822c47b85 100644 --- a/docs/snippets/static11.py +++ b/docs/snippets/static11.py @@ -37,7 +37,7 @@ async def update(self, attr: AttrR[NumberT, TemperatureControllerAttributeIORef] response = await self._connection.send_query(f"{query}\r\n") value = response.strip("\r\n") - await attr.set(attr.dtype(value)) + await attr.update(attr.dtype(value)) async def send( self, attr: AttrW[NumberT, TemperatureControllerAttributeIORef], value: NumberT diff --git a/docs/snippets/static12.py b/docs/snippets/static12.py index 841a5b96a..669cfd8e8 100644 --- a/docs/snippets/static12.py +++ b/docs/snippets/static12.py @@ -39,7 +39,7 @@ async def update(self, attr: AttrR[NumberT, TemperatureControllerAttributeIORef] response = await self._connection.send_query(f"{query}\r\n") value = response.strip("\r\n") - await attr.set(attr.dtype(value)) + await attr.update(attr.dtype(value)) async def send( self, attr: AttrW[NumberT, TemperatureControllerAttributeIORef], value: NumberT @@ -94,7 +94,7 @@ async def update_voltages(self): (await self._connection.send_query("V?\r\n")).strip("\r\n") ) for index, controller in enumerate(self._ramp_controllers): - await controller.voltage.set(float(voltages[index])) + await controller.voltage.update(float(voltages[index])) gui_options = EpicsGUIOptions( diff --git a/docs/snippets/static13.py b/docs/snippets/static13.py index 370207b45..b5586e400 100644 --- a/docs/snippets/static13.py +++ b/docs/snippets/static13.py @@ -40,7 +40,7 @@ async def update(self, attr: AttrR[NumberT, TemperatureControllerAttributeIORef] response = await self._connection.send_query(f"{query}\r\n") value = response.strip("\r\n") - await attr.set(attr.dtype(value)) + await attr.update(attr.dtype(value)) async def send( self, attr: AttrW[NumberT, TemperatureControllerAttributeIORef], value: NumberT @@ -95,12 +95,12 @@ async def update_voltages(self): (await self._connection.send_query("V?\r\n")).strip("\r\n") ) for index, controller in enumerate(self._ramp_controllers): - await controller.voltage.set(float(voltages[index])) + await controller.voltage.update(float(voltages[index])) @command() async def disable_all(self) -> None: for rc in self._ramp_controllers: - await rc.enabled.process(OnOffEnum.Off) + await rc.enabled.put(OnOffEnum.Off, sync_setpoint=True) # TODO: The requests all get concatenated and the sim doesn't handle it await asyncio.sleep(0.1) diff --git a/src/fastcs/attribute_io.py b/src/fastcs/attribute_io.py index fbf30d7b9..ed7c36f78 100644 --- a/src/fastcs/attribute_io.py +++ b/src/fastcs/attribute_io.py @@ -1,7 +1,7 @@ from typing import Any, Generic, cast, get_args from fastcs.attribute_io_ref import AttributeIORef, AttributeIORefT -from fastcs.attributes import AttrR, AttrRW +from fastcs.attributes import AttrR, AttrW from fastcs.datatypes import T from fastcs.tracer import Tracer @@ -32,7 +32,7 @@ def __init__(self): async def update(self, attr: AttrR[T, AttributeIORefT]) -> None: raise NotImplementedError() - async def send(self, attr: AttrRW[T, AttributeIORefT], value: T) -> None: + async def send(self, attr: AttrW[T, AttributeIORefT], value: T) -> None: raise NotImplementedError() diff --git a/src/fastcs/attribute_io_ref.py b/src/fastcs/attribute_io_ref.py index 21e61e451..0cd2b22c8 100644 --- a/src/fastcs/attribute_io_ref.py +++ b/src/fastcs/attribute_io_ref.py @@ -20,5 +20,5 @@ class AttributeIORef: AttributeIORefT = TypeVar( - "AttributeIORefT", bound=AttributeIORef, default=AttributeIORef + "AttributeIORefT", bound=AttributeIORef, default=AttributeIORef, covariant=True ) diff --git a/src/fastcs/attributes.py b/src/fastcs/attributes.py index ac0086f28..6c5fd25a8 100644 --- a/src/fastcs/attributes.py +++ b/src/fastcs/attributes.py @@ -1,22 +1,19 @@ from __future__ import annotations import asyncio -from collections.abc import Callable -from typing import Generic +from collections.abc import Awaitable, Callable +from typing import Any, Generic from fastcs.attribute_io_ref import AttributeIORefT -from fastcs.datatypes import ( - ATTRIBUTE_TYPES, - AttrSetCallback, - AttrUpdateCallback, - DataType, - T, -) +from fastcs.datatypes import ATTRIBUTE_TYPES, DataType, T +from fastcs.logging import logger as _logger from fastcs.tracer import Tracer ONCE = float("inf") """Special value to indicate that an attribute should be updated once on start up.""" +logger = _logger.bind(logger_name=__name__) + class Attribute(Generic[T, AttributeIORefT], Tracer): """Base FastCS attribute. @@ -97,6 +94,14 @@ def __repr__(self): return f"{self.__class__.__name__}({self._name}, {self._datatype})" +AttrIOUpdateCallback = Callable[["AttrR[T, Any]"], Awaitable[None]] +"""An AttributeIO callback that takes an AttrR and updates its value""" +AttrUpdateCallback = Callable[[], Awaitable[None]] +"""A callback to be called periodically to update an attribute""" +AttrOnUpdateCallback = Callable[[T], Awaitable[None]] +"""A callback to be called when the value of the attribute is updated""" + + class AttrR(Attribute[T, AttributeIORefT]): """A read-only ``Attribute``.""" @@ -108,42 +113,80 @@ def __init__( initial_value: T | None = None, description: str | None = None, ) -> None: - super().__init__( - datatype, # type: ignore - io_ref, - group, - description=description, - ) + super().__init__(datatype, io_ref, group, description=description) self._value: T = ( datatype.initial_value if initial_value is None else initial_value ) - self._on_set_callbacks: list[AttrSetCallback[T]] | None = None - self._on_update_callbacks: list[AttrUpdateCallback] | None = None + self._update_callback: AttrIOUpdateCallback[T] | None = None + """Callback to update the value of the attribute with an IO to the source""" + self._on_update_callbacks: list[AttrOnUpdateCallback[T]] | None = None + """Callbacks to publish changes to the value of the attribute""" def get(self) -> T: + """Get the cached value of the attribute.""" return self._value - async def set(self, value: T) -> None: + async def update(self, value: T) -> None: + """Update the value of the attibute + + This sets the cached value of the attribute presented in the API. It should + generally only be called from an IO or a controller that is updating the value + from some underlying source. + + To request a change to the setpoint of the attribute, use the ``put`` method, + which will attempt to apply the change to the underlying source. + + """ self.log_event("Attribute set", attribute=self, value=value) self._value = self._datatype.validate(value) - if self._on_set_callbacks is not None: - await asyncio.gather(*[cb(self._value) for cb in self._on_set_callbacks]) + if self._on_update_callbacks is not None: + await asyncio.gather(*[cb(self._value) for cb in self._on_update_callbacks]) + + def add_on_update_callback(self, callback: AttrOnUpdateCallback[T]) -> None: + """Add a callback to be called when the value of the attribute is updated - def add_set_callback(self, callback: AttrSetCallback[T]) -> None: - if self._on_set_callbacks is None: - self._on_set_callbacks = [] - self._on_set_callbacks.append(callback) + The callback will be called with the updated value. - def add_update_callback(self, callback: AttrUpdateCallback): + """ if self._on_update_callbacks is None: self._on_update_callbacks = [] self._on_update_callbacks.append(callback) - async def update(self): - if self._on_update_callbacks is not None: - await asyncio.gather(*[cb() for cb in self._on_update_callbacks]) + def set_update_callback(self, callback: AttrIOUpdateCallback[T]): + """Set the callback to update the value of the attribute from the source + + The callback will be converted to an async task and called periodically. + + """ + if self._update_callback is not None: + raise RuntimeError("Attribute already has an IO update callback") + + self._update_callback = callback + + def bind_update_callback(self) -> AttrUpdateCallback: + """Bind self into the registered IO update callback""" + if self._update_callback is None: + raise RuntimeError("Attribute has no update callback") + else: + update_callback = self._update_callback + + async def update_attribute(): + try: + self.log_event("Update attribute", topic=self) + await update_callback(self) + except Exception: + logger.opt(exception=True).error("Update loop failed", attribute=self) + raise + + return update_attribute + + +AttrOnPutCallback = Callable[["AttrW[T, Any]", T], Awaitable[None]] +"""Callbacks to be called when the setpoint of an attribute is changed""" +AttrSyncSetpointCallback = Callable[[T], Awaitable[None]] +"""Callbacks to be called when the setpoint of an attribute is changed""" class AttrW(Attribute[T, AttributeIORefT]): @@ -162,41 +205,83 @@ def __init__( group, description=description, ) - self._process_callbacks: list[AttrSetCallback[T]] | None = None - self._write_display_callbacks: list[AttrSetCallback[T]] | None = None + self._on_put_callback: AttrOnPutCallback[T] | None = None + """Callback to action a change to the setpoint of the attribute""" + self._sync_setpoint_callbacks: list[AttrSyncSetpointCallback[T]] = [] + """Callbacks to publish changes to the setpoint of the attribute""" + + async def put(self, setpoint: T, sync_setpoint: bool = False) -> None: + """Set the setpoint of the attribute + + This should be called by clients to the attribute such as transports to apply a + change to the attribute. The ``_on_put_callback`` will be called with this new + setpoint, which may or may not take effect depending on the validity of the new + value. For example, if the attribute has an IO to some device, the value might + be rejected. + + To directly change the value of the attribute, for example from an update loop + that has read a new value from some underlying source, call the ``set`` method. + + """ + setpoint = self._datatype.validate(setpoint) + if self._on_put_callback is not None: + await self._on_put_callback(self, setpoint) + + if sync_setpoint: + await self._call_sync_setpoint_callbacks(setpoint) + + async def _call_sync_setpoint_callbacks(self, setpoint: T) -> None: + if self._sync_setpoint_callbacks: + await asyncio.gather( + *[cb(setpoint) for cb in self._sync_setpoint_callbacks] + ) + + def set_on_put_callback(self, callback: AttrOnPutCallback[T]) -> None: + """Set the callback to call when the setpoint is changed - async def process(self, value: T) -> None: - await self.process_without_display_update(value) - await self.update_display_without_process(value) + The callback will be called with the attribute and the new setpoint. - async def process_without_display_update(self, value: T) -> None: - value = self._datatype.validate(value) - if self._process_callbacks: - await asyncio.gather(*[cb(value) for cb in self._process_callbacks]) + """ + if self._on_put_callback is not None: + raise RuntimeError("Attribute already has an on put callback") - async def update_display_without_process(self, value: T) -> None: - value = self._datatype.validate(value) - if self._write_display_callbacks: - await asyncio.gather(*[cb(value) for cb in self._write_display_callbacks]) + self._on_put_callback = callback - def add_process_callback(self, callback: AttrSetCallback[T]) -> None: - if self._process_callbacks is None: - self._process_callbacks = [] - self._process_callbacks.append(callback) + def add_sync_setpoint_callback(self, callback: AttrSyncSetpointCallback[T]) -> None: + """Add a callback to publish changes to the setpoint of the attribute - def has_process_callback(self) -> bool: - return bool(self._process_callbacks) + The callback will be called with the new setpoint. - def add_write_display_callback(self, callback: AttrSetCallback[T]) -> None: - if self._write_display_callbacks is None: - self._write_display_callbacks = [] - self._write_display_callbacks.append(callback) + """ + self._sync_setpoint_callbacks.append(callback) class AttrRW(AttrR[T, AttributeIORefT], AttrW[T, AttributeIORefT]): """A read-write ``Attribute``.""" - async def process(self, value: T) -> None: - await self.set(value) + def __init__( + self, + datatype: DataType[T], + io_ref: AttributeIORefT | None = None, + group: str | None = None, + initial_value: T | None = None, + description: str | None = None, + ): + super().__init__(datatype, io_ref, group, initial_value, description) + + self._setpoint_initialised = False + + if io_ref is None: + self.set_on_put_callback(self._internal_update) + + async def _internal_update(self, attr: AttrW[T, AttributeIORefT], value: T): + """Update value directly when Attribute has no IO""" + assert attr is self + await self.update(value) + + async def update(self, value: T): + await super().update(value) - await super().process(value) # type: ignore + if not self._setpoint_initialised: + await self._call_sync_setpoint_callbacks(value) + self._setpoint_initialised = True diff --git a/src/fastcs/controller.py b/src/fastcs/controller.py index 9e1fb1c7c..f6e9dc8a2 100755 --- a/src/fastcs/controller.py +++ b/src/fastcs/controller.py @@ -7,7 +7,11 @@ from fastcs.attribute_io import AttributeIO from fastcs.attribute_io_ref import AttributeIORefT -from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW +from fastcs.attributes import ( + Attribute, + AttrR, + AttrW, +) from fastcs.datatypes import T from fastcs.tracer import Tracer @@ -58,39 +62,20 @@ async def attribute_initialise(self) -> None: def _add_io_callbacks(self): for attr in self.attributes.values(): ref = attr.io_ref if attr.has_io_ref() else None + if ref is None: + continue + io = self._attribute_ref_io_map.get(type(ref)) + if io is None: + raise ValueError( + f"{self.__class__.__name__} does not have an AttributeIO " + f"to handle {attr.io_ref.__class__.__name__}" + ) + if isinstance(attr, AttrW): - attr.add_process_callback(self._create_send_callback(io, attr, ref)) + attr.set_on_put_callback(io.send) if isinstance(attr, AttrR): - attr.add_update_callback(self._create_update_callback(io, attr, ref)) - - def _create_send_callback(self, io, attr, ref): - if ref is None: - - async def send_callback(value): - await attr.update_display_without_process(value) - if isinstance(attr, AttrRW): - await attr.set(value) - else: - - async def send_callback(value): - await io.send(attr, value) - - return send_callback - - def _create_update_callback(self, io, attr, ref): - if ref is None: - - async def error_callback(): - raise RuntimeError("Can't call update on Attributes without an io_ref") - - return error_callback - else: - - async def update_callback(): - await io.update(attr) - - return update_callback + attr.set_update_callback(io.update) @property def path(self) -> list[str]: @@ -145,14 +130,6 @@ def _validate_io(self, ios: Sequence[AttributeIO[T, AttributeIORefT]]): f"More than one AttributeIO class handles {ref_type.__name__}" ) - for attr in self.attributes.values(): - if not attr.has_io_ref(): - continue - assert type(attr.io_ref) in self._attribute_ref_io_map, ( - f"{self.__class__.__name__} does not have an AttributeIO to handle " - f"{attr.io_ref.__class__.__name__}" - ) - def add_attribute(self, name, attribute: Attribute): if name in self.attributes and attribute is not self.attributes[name]: raise ValueError( diff --git a/src/fastcs/cs_methods.py b/src/fastcs/cs_methods.py index 1c9c6751c..96dad990b 100644 --- a/src/fastcs/cs_methods.py +++ b/src/fastcs/cs_methods.py @@ -22,7 +22,7 @@ CommandCallback = Callable[[], Coroutine[None, None, None]] """A Command callback that is bound and can be called without `self`""" ScanCallback = Callable[[], Coroutine[None, None, None]] -"""A Scan callback that is bound and can be called withous `self`""" +"""A Scan callback that is bound and can be called without `self`""" PutCallback = Callable[[Any], Coroutine[None, None, None]] """A Put callback that is bound and can be called without `self`""" diff --git a/src/fastcs/datatypes.py b/src/fastcs/datatypes.py index 5b41e3460..6839d3af9 100644 --- a/src/fastcs/datatypes.py +++ b/src/fastcs/datatypes.py @@ -2,7 +2,6 @@ import enum from abc import abstractmethod -from collections.abc import Awaitable, Callable from dataclasses import dataclass from functools import cached_property from typing import Any, Generic, TypeVar @@ -24,10 +23,6 @@ ATTRIBUTE_TYPES: tuple[type] = T.__constraints__ # type: ignore -AttrSetCallback = Callable[[T], Awaitable[None]] -AttrUpdateCallback = Callable[[], Awaitable[None]] - - @dataclass(frozen=True) class DataType(Generic[T]): """Generic datatype mapping to a python type, with additional metadata.""" diff --git a/src/fastcs/demo/controllers.py b/src/fastcs/demo/controllers.py index f95a6426e..b8226ebb6 100755 --- a/src/fastcs/demo/controllers.py +++ b/src/fastcs/demo/controllers.py @@ -62,7 +62,7 @@ async def update( response=response, ) - await attr.set(attr.dtype(response)) + await attr.update(attr.dtype(response)) class TemperatureController(Controller): @@ -87,7 +87,7 @@ def __init__(self, settings: TemperatureControllerSettings) -> None: @command() async def cancel_all(self) -> None: for rc in self._ramp_controllers: - await rc.enabled.process(OnOffEnum.Off) + await rc.enabled.put(OnOffEnum.Off, sync_setpoint=True) # TODO: The requests all get concatenated and the sim doesn't handle it await asyncio.sleep(0.1) @@ -110,7 +110,7 @@ async def update_voltages(self): query=query, response=voltages, ) - await controller.voltage.set(float(voltages[index])) + await controller.voltage.update(float(voltages[index])) class TemperatureRampController(Controller): diff --git a/src/fastcs/launch.py b/src/fastcs/launch.py index d2f2182e7..f465f3bbd 100644 --- a/src/fastcs/launch.py +++ b/src/fastcs/launch.py @@ -31,7 +31,6 @@ from .controller import BaseController, Controller from .controller_api import ControllerAPI from .cs_methods import Command, Put, Scan -from .datatypes import T from .exceptions import FastCSError, LaunchError from .transport import Transport from .util import validate_hinted_attributes @@ -198,7 +197,7 @@ def _link_put_tasks(controller_api: ControllerAPI) -> None: attribute = controller_api.attributes[name] match attribute: case AttrW(): - attribute.add_process_callback(method.fn) + attribute.set_on_put_callback(method.fn) case _: raise FastCSError( f"Attribute type {type(attribute)} does not" @@ -214,7 +213,7 @@ def _get_scan_and_initial_coros( for controller_api in root_controller_api.walk_api(): _add_scan_method_tasks(scan_dict, controller_api) - _add_attribute_updater_tasks(scan_dict, initial_coros, controller_api) + _add_attribute_update_tasks(scan_dict, initial_coros, controller_api) scan_coros = _get_periodic_scan_coros(scan_dict) return scan_coros, initial_coros @@ -227,7 +226,7 @@ def _add_scan_method_tasks( scan_dict[method.period].append(method.fn) -def _add_attribute_updater_tasks( +def _add_attribute_update_tasks( scan_dict: dict[float, list[Callable]], initial_coros: list[Callable], controller_api: ControllerAPI, @@ -237,23 +236,10 @@ def _add_attribute_updater_tasks( case ( AttrR(_io_ref=AttributeIORef(update_period=update_period)) as attribute ): - callback = _create_updater_callback(attribute) if update_period is ONCE: - initial_coros.append(callback) + initial_coros.append(attribute.bind_update_callback()) elif update_period is not None: - scan_dict[update_period].append(callback) - - -def _create_updater_callback(attribute: AttrR[T]): - async def callback(): - try: - tracer.log_event("Call attribute updater", topic=attribute) - await attribute.update() - except Exception: - logger.opt(exception=True).error("Update loop failed", attribute=attribute) - raise - - return callback + scan_dict[update_period].append(attribute.bind_update_callback()) def _get_periodic_scan_coros(scan_dict: dict[float, list[Callable]]) -> list[Callable]: diff --git a/src/fastcs/transport/epics/ca/ioc.py b/src/fastcs/transport/epics/ca/ioc.py index 40e68044d..41337b597 100644 --- a/src/fastcs/transport/epics/ca/ioc.py +++ b/src/fastcs/transport/epics/ca/ioc.py @@ -10,7 +10,7 @@ from fastcs.controller_api import ControllerAPI from fastcs.cs_methods import Command from fastcs.datatypes import DataType, T -from fastcs.logging import logger as _fastcs_logger +from fastcs.logging import logger as _logger from fastcs.tracer import Tracer from fastcs.transport.epics.ca.util import ( builder_callable_from_attribute, @@ -27,7 +27,7 @@ tracer = Tracer(name=__name__) -logger = _fastcs_logger.bind(logger_name=__name__) +logger = _logger.bind(logger_name=__name__) class EpicsCAIOC: @@ -176,7 +176,7 @@ async def async_record_set(value: T): record = _make_record(pv, attribute) _add_attr_pvi_info(record, pv_prefix, attr_name, "r") - attribute.add_set_callback(async_record_set) + attribute.add_on_update_callback(async_record_set) def _make_record( @@ -213,11 +213,9 @@ def _create_and_link_write_pv( async def on_update(value): logger.info("PV put: {pv} = {value}", pv=pv, value=value) - await attribute.process_without_display_update( - cast_from_epics_type(attribute.datatype, value) - ) + await attribute.put(cast_from_epics_type(attribute.datatype, value)) - async def async_write_display(value: T): + async def set_setpoint_without_process(value: T): tracer.log_event( "PV setpoint set from attribute", topic=attribute, pv=pv, value=value ) @@ -228,7 +226,7 @@ async def async_write_display(value: T): _add_attr_pvi_info(record, pv_prefix, attr_name, "w") - attribute.add_write_display_callback(async_write_display) + attribute.add_sync_setpoint_callback(set_setpoint_without_process) def _create_and_link_command_pvs( diff --git a/src/fastcs/transport/epics/pva/_pv_handlers.py b/src/fastcs/transport/epics/pva/_pv_handlers.py index 8c25418ca..f8be29a77 100644 --- a/src/fastcs/transport/epics/pva/_pv_handlers.py +++ b/src/fastcs/transport/epics/pva/_pv_handlers.py @@ -9,6 +9,7 @@ from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW from fastcs.cs_methods import CommandCallback from fastcs.datatypes import Table +from fastcs.tracer import Tracer from .types import ( MAJOR_ALARM_SEVERITY, @@ -20,6 +21,8 @@ p4p_timestamp_now, ) +tracer = Tracer(name=__name__) + class WritePvHandler: def __init__(self, attr_w: AttrW | AttrRW): @@ -43,7 +46,8 @@ async def put(self, pv: SharedPV, op: ServerOperation): cast_value = cast_from_p4p_value(self._attr_w, raw_value) - await self._attr_w.process_without_display_update(cast_value) + tracer.log_event("PV put", topic=self._attr_w, pv=pv, value=cast_value) + await self._attr_w.put(cast_value) op.done() @@ -115,10 +119,11 @@ def make_shared_read_pv(attribute: AttrR) -> SharedPV: **_make_shared_pv_arguments(attribute), ) - async def on_update(value): + async def set_readback(value): + tracer.log_event("PV set readback", topic=attribute, value=value) shared_pv.post(cast_to_p4p_value(attribute, value)) - attribute.add_set_callback(on_update) + attribute.add_on_update_callback(set_readback) return shared_pv @@ -130,10 +135,11 @@ def make_shared_write_pv(attribute: AttrW) -> SharedPV: **_make_shared_pv_arguments(attribute), ) - async def async_write_display(value): + async def set_setpoint(value): + tracer.log_event("PV set setpoint", topic=attribute, value=value) shared_pv.post(cast_to_p4p_value(attribute, value)) - attribute.add_write_display_callback(async_write_display) + attribute.add_sync_setpoint_callback(set_setpoint) return shared_pv diff --git a/src/fastcs/transport/graphql/graphql.py b/src/fastcs/transport/graphql/graphql.py index dfa3ef242..463993cee 100644 --- a/src/fastcs/transport/graphql/graphql.py +++ b/src/fastcs/transport/graphql/graphql.py @@ -124,7 +124,7 @@ def _wrap_attr_set( """Wrap an attribute in a function with annotations for strawberry""" async def _dynamic_f(value): - await attribute.process(value) + await attribute.put(value) return value # Add type annotations for validation, schema, conversions diff --git a/src/fastcs/transport/rest/rest.py b/src/fastcs/transport/rest/rest.py index ed45ddb57..1a798150e 100644 --- a/src/fastcs/transport/rest/rest.py +++ b/src/fastcs/transport/rest/rest.py @@ -69,13 +69,13 @@ def _put_request_body(attribute: AttrW[T]): def _wrap_attr_put( attribute: AttrW[T], ) -> Callable[[T], Coroutine[Any, Any, None]]: - async def attr_set(request): - await attribute.process(cast_from_rest_type(attribute.datatype, request.value)) + async def attr_put(request): + await attribute.put(cast_from_rest_type(attribute.datatype, request.value)) # Fast api uses type annotations for validation, schema, conversions - attr_set.__annotations__["request"] = _put_request_body(attribute) + attr_put.__annotations__["request"] = _put_request_body(attribute) - return attr_set + return attr_put def _get_response_body(attribute: AttrR[T]): diff --git a/src/fastcs/transport/tango/dsr.py b/src/fastcs/transport/tango/dsr.py index 7686ef36d..992961bad 100644 --- a/src/fastcs/transport/tango/dsr.py +++ b/src/fastcs/transport/tango/dsr.py @@ -50,7 +50,7 @@ def _wrap_updater_fset( ) -> Callable[[Any, Any], Any]: async def fset(tango_device: Device, value): tango_device.info_stream(f"called fset method: {attr_name}") - coro = attribute.process(cast_from_tango_type(attribute.datatype, value)) + coro = attribute.put(cast_from_tango_type(attribute.datatype, value)) await _run_threadsafe_blocking(coro, loop) return fset diff --git a/tests/assertable_controller.py b/tests/assertable_controller.py index 0f5e3a12c..2a25729a3 100644 --- a/tests/assertable_controller.py +++ b/tests/assertable_controller.py @@ -7,7 +7,7 @@ from fastcs.attribute_io import AttributeIO from fastcs.attribute_io_ref import AttributeIORef -from fastcs.attributes import AttrR, AttrW +from fastcs.attributes import AttrR, AttrRW, AttrW from fastcs.controller import Controller from fastcs.controller_api import ControllerAPI from fastcs.datatypes import Int, T @@ -26,6 +26,8 @@ async def update(self, attr: AttrR[T, MyTestAttributeIORef]): async def send(self, attr: AttrW[T, MyTestAttributeIORef], value: T): print(f"sending {attr} = {value}") + if isinstance(attr, AttrRW): + await attr.update(value) test_attribute_io = MyTestAttributeIO() # instance @@ -96,13 +98,13 @@ def assert_read_here(self, path: list[str]): @contextmanager def assert_write_here(self, path: list[str]): - yield from self._assert_method(path, "process") + yield from self._assert_method(path, "put") @contextmanager def assert_execute_here(self, path: list[str]): yield from self._assert_method(path, "") - def _assert_method(self, path: list[str], method: Literal["get", "process", ""]): + def _assert_method(self, path: list[str], method: Literal["get", "put", ""]): """ This context manager can be used to confirm that a fastcs controller's respective attribute or command methods are called diff --git a/tests/conftest.py b/tests/conftest.py index 6cbea4685..28393e926 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,7 +20,8 @@ from fastcs.attributes import AttrR, AttrRW, AttrW from fastcs.datatypes import Bool, Float, Int, String from fastcs.launch import build_controller_api -from fastcs.logging import logger +from fastcs.logging import configure_logging, logger +from fastcs.logging._logging import LogLevel from fastcs.transport.tango.dsr import register_dev from tests.assertable_controller import MyTestAttributeIORef, MyTestController from tests.example_p4p_ioc import run as _run_p4p_ioc @@ -87,11 +88,8 @@ def _run_ioc_as_subprocess( error_queue: multiprocessing.Queue, stdout_queue: multiprocessing.Queue, ): - # configure_logging(LogLevel.INFO) - # we need to capture log messages from transport - # logger = _logger.bind(logger_name="fastcs.transport.epics.ca.transport") - logger.add(print) # forward log messages to stdout - logger.enable("fastcs") + configure_logging(LogLevel.TRACE) + logger.add(print) # forward log messages to stdout for start up detection in tests try: from pytest_cov.embed import cleanup_on_sigterm diff --git a/tests/example_p4p_ioc.py b/tests/example_p4p_ioc.py index 9cd084b50..62cf1a210 100644 --- a/tests/example_p4p_ioc.py +++ b/tests/example_p4p_ioc.py @@ -41,13 +41,13 @@ async def d(self): print("D: RUNNING") await asyncio.sleep(0.1) print("D: FINISHED") - await self.j.set(self.j.get() + 1) + await self.j.update(self.j.get() + 1) e: AttrR = AttrR(Bool()) @scan(1) async def flip_flop(self): - await self.e.set(not self.e.get()) + await self.e.update(not self.e.get()) f: AttrRW = AttrRW(Enum(FEnum)) g: AttrRW = AttrRW(Waveform(np.int64, shape=(3,))) @@ -63,13 +63,14 @@ async def i(self): else: self.fail_on_next_e = True print("I: FINISHED") - await self.j.set(self.j.get() + 1) + await self.j.update(self.j.get() + 1) j: AttrR = AttrR(Int()) def run(pv_prefix="P4P_TEST_DEVICE"): controller = ParentController() + controller.a.enable_tracing() controller.child1 = ChildController(description="some sub controller") controller.child2 = ChildController(description="another sub controller") diff --git a/tests/test_attribute.py b/tests/test_attribute.py index 756e49835..b2247a7d7 100644 --- a/tests/test_attribute.py +++ b/tests/test_attribute.py @@ -26,43 +26,25 @@ async def test_attributes(): async def update_ui(value, key): ui[key] = value - async def send(value, key): + async def send(_attr, value, key): device[key] = value async def device_add(): device["number"] += 1 attr_r = AttrR(String()) - attr_r.add_set_callback(partial(update_ui, key="state")) - await attr_r.set(device["state"]) + attr_r.add_on_update_callback(partial(update_ui, key="state")) + await attr_r.update(device["state"]) assert ui["state"] == "Idle" attr_rw = AttrRW(Int()) - attr_rw.add_process_callback(partial(send, key="number")) - attr_rw.add_write_display_callback(partial(update_ui, key="number")) - await attr_rw.process(2) + attr_rw._on_put_callback = partial(send, key="number") + attr_rw.add_sync_setpoint_callback(partial(update_ui, key="number")) + await attr_rw.put(2, sync_setpoint=True) assert device["number"] == 2 assert ui["number"] == 2 -@pytest.mark.asyncio -async def test_simple_attibute_io_rw(mocker: MockerFixture): - attr = AttrRW(Int()) - - attr.update_display_without_process = mocker.MagicMock( - wraps=attr.update_display_without_process - ) - attr.set = mocker.MagicMock(wraps=attr.set) - - # This is called by the transport when it receives a put - await attr.process(1) - - # without io/ref should just set the value on the attribute - attr.update_display_without_process.assert_called_once_with(1) - attr.set.assert_called_once_with(1) - assert attr.get() == 1 - - @pytest.mark.asyncio async def test_attribute_io(): @dataclass @@ -85,12 +67,20 @@ def __init__(self): class ControllerNoIO(Controller): my_attr = AttrR(Int(), io_ref=MyAttributeIORef(cool=5)) - with pytest.raises(AssertionError, match="does not have an AttributeIO"): - ControllerNoIO() + @dataclass + class OtherAttributeIORef(AttributeIORef): + not_cool: int + + class MissingIOController(Controller): + my_attr = AttrR(Int(), io_ref=OtherAttributeIORef(not_cool=5)) + + with pytest.raises(ValueError, match="does not have an AttributeIO to handle"): + controller = MissingIOController() + controller._add_io_callbacks() await c.initialise() await c.attribute_initialise() - await c.my_attr.update() + await c.my_attr.bind_update_callback()() class DummyConnection: @@ -176,7 +166,7 @@ async def update( attr: AttrR[NumberT, DemoParameterAttributeIORef], ): value = await attr.io_ref.connection.get(attr.io_ref.uri) - await attr.set(value) # type: ignore + await attr.update(value) # type: ignore async def send( self, @@ -231,70 +221,61 @@ async def initialise(self): c = DemoParameterController(ios=[DemoParameterAttributeIO()]) await c.initialise() await c.attribute_initialise() - await c.ro_int_parameter.update() + await c.ro_int_parameter.bind_update_callback()() assert c.ro_int_parameter.get() == 10 - await c.ro_int_parameter.update() + await c.ro_int_parameter.bind_update_callback()() assert c.ro_int_parameter.get() == 11 - await c.int_parameter.process(20) + await c.int_parameter.put(20) assert c.int_parameter.get() == 20 @pytest.mark.asyncio -async def test_attribute_io_defaults(mocker: MockerFixture): +async def test_attribute_no_io(mocker: MockerFixture): class MyController(Controller): no_ref = AttrRW(Int()) base_class_ref = AttrRW(Int(), io_ref=AttributeIORef()) with pytest.raises( - AssertionError, + ValueError, match="MyController does not have an AttributeIO to handle AttributeIORef", ): c = MyController() + await c.attribute_initialise() class SimpleAttributeIO(AttributeIO[T, AttributeIORef]): async def update(self, attr): match attr: case AttrR(datatype=Int()): - await attr.set(100) + await attr.update(100) with pytest.raises( RuntimeError, match="More than one AttributeIO class handles AttributeIORef" ): - MyController(ios=[AttributeIO(), SimpleAttributeIO()]) + MyController(ios=[SimpleAttributeIO(), SimpleAttributeIO()]) # we need to explicitly pass an AttributeIO if we want to handle instances of # the AttributeIORef base class - c = MyController(ios=[AttributeIO()]) + c = MyController(ios=[SimpleAttributeIO()]) assert not c.no_ref.has_io_ref() assert c.base_class_ref.has_io_ref() await c.initialise() await c.attribute_initialise() - with pytest.raises(NotImplementedError): - await c.base_class_ref.update() - - with pytest.raises(NotImplementedError): - await c.base_class_ref.process(25) - # There is a difference between providing an AttributeIO for the default # AttributeIORef class and not specifying the io_ref for an Attribute # default callbacks are not provided by AttributeIO subclasses - with pytest.raises( - RuntimeError, match="Can't call update on Attributes without an io_ref" - ): - await c.no_ref.update() - - process_spy = mocker.spy(c.no_ref, "update_display_without_process") - # calls callback which calls update_display_without_process - # TODO: reconsider if this is what we want the default case to be - # as process already calls that - await c.no_ref.process_without_display_update(40) - process_spy.assert_called_with(40) + sync_setpoint_mock = mocker.AsyncMock() + c.no_ref.add_sync_setpoint_callback(sync_setpoint_mock) - process_spy.assert_called_once_with(40) + await c.no_ref.put(40) + sync_setpoint_mock.assert_called_once_with(40) # sync setpoint called on first set + sync_setpoint_mock.reset_mock() + await c.no_ref.put(41) # sync setpoint callback not called without flag + await c.no_ref.put(42, sync_setpoint=True) + sync_setpoint_mock.assert_called_once_with(42) c2 = MyController(ios=[SimpleAttributeIO()]) @@ -302,5 +283,5 @@ async def update(self, attr): await c2.attribute_initialise() assert c2.base_class_ref.get() == 0 - await c2.base_class_ref.update() + await c2.base_class_ref.bind_update_callback()() assert c2.base_class_ref.get() == 100 diff --git a/tests/test_launch.py b/tests/test_launch.py index 1646f2115..ea17ba20e 100644 --- a/tests/test_launch.py +++ b/tests/test_launch.py @@ -182,8 +182,8 @@ def test_fastcs(controller): assert controller.initialised assert not controller.connected - # Controller Attributes with a Sender should have a _process_callback created - assert controller.read_write_int.has_process_callback() + # Controller Attributes with an IO _send_callback created + assert controller.read_write_int._on_put_callback is not None async def test_wrapper(): loop.create_task(fastcs.serve_routines()) @@ -267,7 +267,7 @@ class AttributeIORefTimesCalled(AttributeIORef): class AttributeIOTimesCalled(AttributeIO[int, AttributeIORefTimesCalled]): async def update(self, attr: AttrR[int, AttributeIORefTimesCalled]): attr.io_ref._times_called += 1 - await attr.set(attr.io_ref._times_called) + await attr.update(attr.io_ref._times_called) class MyController(Controller): update_once = AttrR(Int(), io_ref=AttributeIORefTimesCalled(update_period=ONCE)) diff --git a/tests/transport/epics/ca/test_softioc.py b/tests/transport/epics/ca/test_softioc.py index a1b22556f..109c902de 100644 --- a/tests/transport/epics/ca/test_softioc.py +++ b/tests/transport/epics/ca/test_softioc.py @@ -51,7 +51,7 @@ async def test_create_and_link_read_pv(mocker: MockerFixture): record = make_record.return_value attribute = AttrR(Int()) - attribute.add_set_callback = mocker.MagicMock() + attribute.add_on_update_callback = mocker.MagicMock() _create_and_link_read_pv("PREFIX", "PV", "attr", attribute) @@ -59,8 +59,8 @@ async def test_create_and_link_read_pv(mocker: MockerFixture): add_attr_pvi_info.assert_called_once_with(record, "PREFIX", "attr", "r") # Extract the callback generated and set in the function and call it - attribute.add_set_callback.assert_called_once_with(mocker.ANY) - record_set_callback = attribute.add_set_callback.call_args[0][0] + attribute.add_on_update_callback.assert_called_once_with(mocker.ANY) + record_set_callback = attribute.add_on_update_callback.call_args[0][0] await record_set_callback(1) record.set.assert_called_once_with(1) @@ -115,8 +115,8 @@ async def test_create_and_link_write_pv(mocker: MockerFixture): record = make_record.return_value attribute = AttrW(Int()) - attribute.process_without_display_update = mocker.AsyncMock() - attribute.add_write_display_callback = mocker.MagicMock() + attribute.put = mocker.AsyncMock() + attribute.add_sync_setpoint_callback = mocker.MagicMock() _create_and_link_write_pv("PREFIX", "PV", "attr", attribute) @@ -126,9 +126,9 @@ async def test_create_and_link_write_pv(mocker: MockerFixture): add_attr_pvi_info.assert_called_once_with(record, "PREFIX", "attr", "w") # Extract the write update callback generated and set in the function and call it - attribute.add_write_display_callback.assert_called_once_with(mocker.ANY) - write_display_callback = attribute.add_write_display_callback.call_args[0][0] - await write_display_callback(1) + attribute.add_sync_setpoint_callback.assert_called_once_with(mocker.ANY) + sync_setpoint_callback = attribute.add_sync_setpoint_callback.call_args[0][0] + await sync_setpoint_callback(1) record.set.assert_called_once_with(1, process=False) @@ -136,7 +136,7 @@ async def test_create_and_link_write_pv(mocker: MockerFixture): on_update_callback = make_record.call_args[1]["on_update"] await on_update_callback(1) - attribute.process_without_display_update.assert_called_once_with(1) + attribute.put.assert_called_once_with(1) class LongEnum(enum.Enum): diff --git a/tests/transport/epics/pva/test_p4p.py b/tests/transport/epics/pva/test_p4p.py index f7c338321..6b93efd21 100644 --- a/tests/transport/epics/pva/test_p4p.py +++ b/tests/transport/epics/pva/test_p4p.py @@ -168,11 +168,9 @@ async def test_command_method(p4p_subprocess: tuple[str, Queue]): async def test_numerical_alarms(p4p_subprocess: tuple[str, Queue]): pv_prefix, _ = p4p_subprocess a_values = asyncio.Queue() - b_values = asyncio.Queue() ctxt = Context("pva") - a_monitor = ctxt.monitor(f"{pv_prefix}:A", a_values.put) - b_monitor = ctxt.monitor(f"{pv_prefix}:B", b_values.put) + a_monitor = ctxt.monitor(f"{pv_prefix}:A_RBV", a_values.put) try: value = (await a_values.get()).raw @@ -181,14 +179,7 @@ async def test_numerical_alarms(p4p_subprocess: tuple[str, Queue]): assert value["alarm"]["severity"] == 0 assert value["alarm"]["message"] == "No alarm" - value = (await b_values.get()).raw - assert value["value"] == 0 - assert isinstance(value["value"], float) - assert value["alarm"]["severity"] == 0 - assert value["alarm"]["message"] == "No alarm" - await ctxt.put(f"{pv_prefix}:A", 40_001) - await ctxt.put(f"{pv_prefix}:B", -0.6) value = (await a_values.get()).raw assert value["value"] == 40_001 @@ -196,14 +187,7 @@ async def test_numerical_alarms(p4p_subprocess: tuple[str, Queue]): assert value["alarm"]["severity"] == 2 assert value["alarm"]["message"] == "Above maximum alarm limit: 40000" - value = (await b_values.get()).raw - assert value["value"] == -0.6 - assert isinstance(value["value"], float) - assert value["alarm"]["severity"] == 2 - assert value["alarm"]["message"] == "Below minimum alarm limit: -0.5" - await ctxt.put(f"{pv_prefix}:A", 40_000) - await ctxt.put(f"{pv_prefix}:B", -0.5) value = (await a_values.get()).raw assert value["value"] == 40_000 @@ -211,18 +195,10 @@ async def test_numerical_alarms(p4p_subprocess: tuple[str, Queue]): assert value["alarm"]["severity"] == 0 assert value["alarm"]["message"] == "No alarm" - value = (await b_values.get()).raw - assert value["value"] == -0.5 - assert isinstance(value["value"], float) - assert value["alarm"]["severity"] == 0 - assert value["alarm"]["message"] == "No alarm" - assert a_values.empty() - assert b_values.empty() finally: a_monitor.close() - b_monitor.close() def make_fastcs(pv_prefix: str, controller: Controller) -> FastCS: @@ -244,12 +220,12 @@ class SomeController(Controller): async def _wait_and_set_attr_r(): await asyncio.sleep(0.05) - await controller.a.set(40_000) - await controller.b.set(-0.99) + await controller.a.update(40_000) + await controller.b.update(-0.99) await asyncio.sleep(0.05) - await controller.a.set(-100) - await controller.b.set(-0.99) - await controller.b.set(-0.9111111) + await controller.a.update(-100) + await controller.b.update(-0.99) + await controller.b.update(-0.9111111) a_values, b_values = [], [] a_monitor = ctxt.monitor(f"{pv_prefix}:A_RBV", a_values.append) @@ -444,9 +420,9 @@ async def _wait_and_set_attrs(): # This demonstrates an update from hardware, # resulting in only a change in the read back. await asyncio.gather( - controller.some_waveform.set(server_set_waveform_value), - controller.some_table.set(server_set_table_value), - controller.some_enum.set(server_set_enum_value), + controller.some_waveform.update(server_set_waveform_value), + controller.some_table.update(server_set_table_value), + controller.some_enum.update(server_set_enum_value), ) async def _wait_and_put_pvs(): diff --git a/tests/transport/rest/test_rest.py b/tests/transport/rest/test_rest.py index 1d67d39b5..6db79a2b5 100644 --- a/tests/transport/rest/test_rest.py +++ b/tests/transport/rest/test_rest.py @@ -5,11 +5,7 @@ import pytest from fastapi.testclient import TestClient from pytest_mock import MockerFixture -from tests.assertable_controller import ( - AssertableControllerAPI, - MyTestAttributeIORef, - MyTestController, -) +from tests.assertable_controller import AssertableControllerAPI, MyTestController from fastcs.attributes import AttrR, AttrRW, AttrW from fastcs.controller_api import ControllerAPI @@ -18,11 +14,11 @@ class RestController(MyTestController): - read_int = AttrR(Int(), io_ref=MyTestAttributeIORef()) - read_write_int = AttrRW(Int(), io_ref=MyTestAttributeIORef()) + read_int = AttrR(Int()) + read_write_int = AttrRW(Int()) read_write_float = AttrRW(Float()) read_bool = AttrR(Bool()) - write_bool = AttrW(Bool(), io_ref=MyTestAttributeIORef()) + write_bool = AttrW(Bool()) read_string = AttrRW(String()) enum = AttrRW(Enum(enum.IntEnum("Enum", {"RED": 0, "GREEN": 1, "BLUE": 2}))) one_d_waveform = AttrRW(Waveform(np.int32, (10,))) diff --git a/tests/transport/tango/test_dsr.py b/tests/transport/tango/test_dsr.py index 6c22e17f4..1feb4b33d 100644 --- a/tests/transport/tango/test_dsr.py +++ b/tests/transport/tango/test_dsr.py @@ -8,7 +8,6 @@ from tango.test_context import DeviceTestContext from tests.assertable_controller import ( AssertableControllerAPI, - MyTestAttributeIORef, MyTestController, ) @@ -31,11 +30,11 @@ def mock_run_threadsafe_blocking(module_mocker: MockerFixture): class TangoController(MyTestController): - read_int = AttrR(Int(), io_ref=MyTestAttributeIORef()) - read_write_int = AttrRW(Int(), io_ref=MyTestAttributeIORef()) + read_int = AttrR(Int()) + read_write_int = AttrRW(Int()) read_write_float = AttrRW(Float()) read_bool = AttrR(Bool()) - write_bool = AttrW(Bool(), io_ref=MyTestAttributeIORef()) + write_bool = AttrW(Bool()) read_string = AttrRW(String()) enum = AttrRW(Enum(enum.IntEnum("Enum", {"RED": 0, "GREEN": 1, "BLUE": 2}))) one_d_waveform = AttrRW(Waveform(np.int32, (10,)))