From 56d00b7f00cae4a7b41b6251c548f8b98bc6ac17 Mon Sep 17 00:00:00 2001 From: Alan Greer Date: Wed, 12 Mar 2025 14:56:33 +0000 Subject: [PATCH 01/17] Added default initialise methods to Sender and Updater base classes. Added initialise method to Attribute, checks for handler and calls the method. Moved initialise into BaseController class, loops over attributes calling initialise with a reference to self. Added unit test for Attribute class. --- src/fastcs/attributes.py | 19 +++++++++++++++++++ src/fastcs/controller.py | 8 +++++--- tests/test_attribute.py | 14 +++++++++++++- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/fastcs/attributes.py b/src/fastcs/attributes.py index a36a00e48..cb3687ed5 100644 --- a/src/fastcs/attributes.py +++ b/src/fastcs/attributes.py @@ -22,9 +22,16 @@ class AttrMode(Enum): class Sender(Protocol): """Protocol for setting the value of an ``Attribute``.""" +<<<<<<< HEAD async def put( self, controller: fastcs.controller.BaseController, attr: AttrW, value: Any ) -> None: +======= + async def initialise(self, controller: Any): + pass + + async def put(self, controller: Any, attr: AttrW, value: Any) -> None: +>>>>>>> 43721ff (Added default initialise methods to Sender and Updater base classes. Added initialise method to Attribute, checks for handler and calls the method. Moved initialise into BaseController class, loops over attributes calling initialise with a reference to self. Added unit test for Attribute class.) pass @@ -35,9 +42,16 @@ class Updater(Protocol): # If update period is None then the attribute will not be updated as a task. update_period: float | None = None +<<<<<<< HEAD async def update( self, controller: fastcs.controller.BaseController, attr: AttrR ) -> None: +======= + async def initialise(self, controller: Any): + pass + + async def update(self, controller: Any, attr: AttrR) -> None: +>>>>>>> 43721ff (Added default initialise methods to Sender and Updater base classes. Added initialise method to Attribute, checks for handler and calls the method. Moved initialise into BaseController class, loops over attributes calling initialise with a reference to self. Added unit test for Attribute class.) pass @@ -84,6 +98,7 @@ def __init__( self._datatype: DataType[T] = datatype self._access_mode: AttrMode = access_mode self._group = group + self._handler = handler self.enabled = True self.description = description @@ -107,6 +122,10 @@ def access_mode(self) -> AttrMode: def group(self) -> str | None: return self._group + async def initialise(self, controller: Any) -> None: + if self._handler is not None: + await self._handler.initialise(controller) + def add_update_datatype_callback( self, callback: Callable[[DataType[T]], None] ) -> None: diff --git a/src/fastcs/controller.py b/src/fastcs/controller.py index c9351a903..0b2767a5c 100755 --- a/src/fastcs/controller.py +++ b/src/fastcs/controller.py @@ -29,6 +29,11 @@ def __init__( self._bind_attrs() + async def initialise(self) -> None: + # Loop over attributes and initialise any registered handlers + for attr in self.attributes.values(): + await attr.initialise(self) + @property def path(self) -> list[str]: """Path prefix of attributes, recursively including parent Controllers.""" @@ -116,9 +121,6 @@ class Controller(BaseController): def __init__(self, description: str | None = None) -> None: super().__init__(description=description) - async def initialise(self) -> None: - pass - async def connect(self) -> None: pass diff --git a/tests/test_attribute.py b/tests/test_attribute.py index b00fb94a4..f0300196a 100644 --- a/tests/test_attribute.py +++ b/tests/test_attribute.py @@ -4,7 +4,7 @@ import pytest from pytest_mock import MockerFixture -from fastcs.attributes import AttrR, AttrRW, AttrW +from fastcs.attributes import AttrR, AttrRW, AttrW, SimpleHandler from fastcs.datatypes import Enum, Float, Int, String, Waveform @@ -60,6 +60,18 @@ async def test_simple_handler_rw(mocker: MockerFixture): set_mock.assert_awaited_once_with(1) +@pytest.mark.asyncio +async def test_handler_initialise(mocker: MockerFixture): + handler = SimpleHandler() + handler_mock = mocker.patch.object(handler, "initialise") + attr = AttrR(Int(), handler=handler) + + await attr.initialise(mocker.ANY) + + # The handler initialise method should be called from the attribute + handler_mock.assert_called_once_with(1) + + @pytest.mark.parametrize( ["datatype", "init_args", "value"], [ From 3b8f987a5abaf64b58b0c59283bb3f7f6e306025 Mon Sep 17 00:00:00 2001 From: Alan Greer Date: Fri, 14 Mar 2025 16:15:18 +0000 Subject: [PATCH 02/17] Use asyncio gather for attribute initialisation. --- src/fastcs/controller.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/fastcs/controller.py b/src/fastcs/controller.py index 0b2767a5c..368c01d2e 100755 --- a/src/fastcs/controller.py +++ b/src/fastcs/controller.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio from copy import copy from typing import get_type_hints @@ -30,9 +31,12 @@ def __init__( self._bind_attrs() async def initialise(self) -> None: - # Loop over attributes and initialise any registered handlers - for attr in self.attributes.values(): - await attr.initialise(self) + # Initialise any registered handlers for attributes + coros = [attr.initialise(self) for attr in self.attributes.values()] + try: + await asyncio.gather(*coros) + except asyncio.CancelledError: + pass @property def path(self) -> list[str]: From 26fc21264791aa48538b87b73c1d5a278024d149 Mon Sep 17 00:00:00 2001 From: Alan Greer Date: Fri, 14 Mar 2025 16:33:18 +0000 Subject: [PATCH 03/17] Split initialise method and store controller into base class --- src/fastcs/attributes.py | 48 +++++++++++++++++++++++----------------- tests/test_attribute.py | 4 ++-- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/fastcs/attributes.py b/src/fastcs/attributes.py index cb3687ed5..270b73d4a 100644 --- a/src/fastcs/attributes.py +++ b/src/fastcs/attributes.py @@ -22,16 +22,20 @@ class AttrMode(Enum): class Sender(Protocol): """Protocol for setting the value of an ``Attribute``.""" -<<<<<<< HEAD - async def put( - self, controller: fastcs.controller.BaseController, attr: AttrW, value: Any - ) -> None: -======= - async def initialise(self, controller: Any): + # Record the controller that owns this attribute handler + controller: Any = None + + async def initialise(self, controller: Any) -> None: + # Register the controller + self.controller = controller + + # Continue with initialisation + await self._initialise() + + async def _initialise(self) -> None: pass - async def put(self, controller: Any, attr: AttrW, value: Any) -> None: ->>>>>>> 43721ff (Added default initialise methods to Sender and Updater base classes. Added initialise method to Attribute, checks for handler and calls the method. Moved initialise into BaseController class, loops over attributes calling initialise with a reference to self. Added unit test for Attribute class.) + async def put(self, attr: AttrW, value: Any) -> None: pass @@ -39,19 +43,25 @@ async def put(self, controller: Any, attr: AttrW, value: Any) -> None: class Updater(Protocol): """Protocol for updating the cached readback value of an ``Attribute``.""" + # Record the controller that owns this attribute handler + controller: Any = None + # If update period is None then the attribute will not be updated as a task. update_period: float | None = None -<<<<<<< HEAD - async def update( - self, controller: fastcs.controller.BaseController, attr: AttrR - ) -> None: -======= - async def initialise(self, controller: Any): + async def initialise(self, controller: Any) -> None: + pass + + # Register the controller + self.controller = controller + + # Continue with initialisation + await self._initialise() + + async def _initialise(self) -> None: pass - async def update(self, controller: Any, attr: AttrR) -> None: ->>>>>>> 43721ff (Added default initialise methods to Sender and Updater base classes. Added initialise method to Attribute, checks for handler and calls the method. Moved initialise into BaseController class, loops over attributes calling initialise with a reference to self. Added unit test for Attribute class.) + async def update(self, attr: AttrR) -> None: pass @@ -65,15 +75,13 @@ class Handler(Sender, Updater, Protocol): class SimpleHandler(Handler): """Handler for internal parameters""" - async def put( - self, controller: fastcs.controller.BaseController, attr: AttrW, value: Any - ): + async def put(self, attr: AttrW, value: Any): await attr.update_display_without_process(value) if isinstance(attr, AttrRW): await attr.set(value) - async def update(self, controller: Any, attr: AttrR): + async def update(self, attr: AttrR): raise RuntimeError("SimpleHandler cannot update") diff --git a/tests/test_attribute.py b/tests/test_attribute.py index f0300196a..b8bc683c8 100644 --- a/tests/test_attribute.py +++ b/tests/test_attribute.py @@ -41,7 +41,7 @@ async def test_simple_handler_w(mocker: MockerFixture): update_display_mock = mocker.patch.object(attr, "update_display_without_process") # This is called by the transport when it receives a put - await attr.sender.put(mocker.ANY, attr, 1) + await attr.sender.put(attr, 1) # The callback to update the transport display should be called update_display_mock.assert_called_once_with(1) @@ -53,7 +53,7 @@ async def test_simple_handler_rw(mocker: MockerFixture): update_display_mock = mocker.patch.object(attr, "update_display_without_process") set_mock = mocker.patch.object(attr, "set") - await attr.sender.put(mocker.ANY, attr, 1) + await attr.sender.put(attr, 1) update_display_mock.assert_called_once_with(1) # The Sender of the attribute should just set the value on the attribute From 526c769edd36b6b6acb8a4bd49d38f0aaff3912d Mon Sep 17 00:00:00 2001 From: Alan Greer Date: Mon, 24 Mar 2025 17:37:51 +0000 Subject: [PATCH 04/17] Removed controller registration from Handler --- src/fastcs/attributes.py | 41 +++++++++------------------------------- 1 file changed, 9 insertions(+), 32 deletions(-) diff --git a/src/fastcs/attributes.py b/src/fastcs/attributes.py index 270b73d4a..a93d27c0b 100644 --- a/src/fastcs/attributes.py +++ b/src/fastcs/attributes.py @@ -22,47 +22,21 @@ class AttrMode(Enum): class Sender(Protocol): """Protocol for setting the value of an ``Attribute``.""" - # Record the controller that owns this attribute handler - controller: Any = None + async def initialise(self, controller: Any) -> None: ... - async def initialise(self, controller: Any) -> None: - # Register the controller - self.controller = controller - - # Continue with initialisation - await self._initialise() - - async def _initialise(self) -> None: - pass - - async def put(self, attr: AttrW, value: Any) -> None: - pass + async def put(self, attr: AttrW[T], value: T) -> None: ... @runtime_checkable class Updater(Protocol): """Protocol for updating the cached readback value of an ``Attribute``.""" - # Record the controller that owns this attribute handler - controller: Any = None - # If update period is None then the attribute will not be updated as a task. update_period: float | None = None - async def initialise(self, controller: Any) -> None: - pass + async def initialise(self, controller: Any) -> None: ... - # Register the controller - self.controller = controller - - # Continue with initialisation - await self._initialise() - - async def _initialise(self) -> None: - pass - - async def update(self, attr: AttrR) -> None: - pass + async def update(self, attr: AttrR) -> None: ... @runtime_checkable @@ -75,13 +49,16 @@ class Handler(Sender, Updater, Protocol): class SimpleHandler(Handler): """Handler for internal parameters""" - async def put(self, attr: AttrW, value: Any): + async def initialise(self, controller: Any) -> None: + pass + + async def put(self, attr: AttrW[T], value: T) -> None: await attr.update_display_without_process(value) if isinstance(attr, AttrRW): await attr.set(value) - async def update(self, attr: AttrR): + async def update(self, attr: AttrR) -> None: raise RuntimeError("SimpleHandler cannot update") From c2427f15a2a09e428161ce7df79ddd731cae8e5a Mon Sep 17 00:00:00 2001 From: Alan Greer Date: Wed, 26 Mar 2025 11:01:44 +0000 Subject: [PATCH 05/17] Added type hints for controller. Removed initialise from SimpleHandler. --- src/fastcs/attributes.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/fastcs/attributes.py b/src/fastcs/attributes.py index a93d27c0b..ccbb0cd7c 100644 --- a/src/fastcs/attributes.py +++ b/src/fastcs/attributes.py @@ -22,9 +22,11 @@ class AttrMode(Enum): class Sender(Protocol): """Protocol for setting the value of an ``Attribute``.""" - async def initialise(self, controller: Any) -> None: ... + async def initialise(self, controller: fastcs.controller.BaseController) -> None: + pass - async def put(self, attr: AttrW[T], value: T) -> None: ... + async def put(self, attr: AttrW[T], value: T) -> None: + pass @runtime_checkable @@ -34,9 +36,11 @@ class Updater(Protocol): # If update period is None then the attribute will not be updated as a task. update_period: float | None = None - async def initialise(self, controller: Any) -> None: ... + async def initialise(self, controller: fastcs.controller.BaseController) -> None: + pass - async def update(self, attr: AttrR) -> None: ... + async def update(self, attr: AttrR) -> None: + pass @runtime_checkable @@ -49,9 +53,6 @@ class Handler(Sender, Updater, Protocol): class SimpleHandler(Handler): """Handler for internal parameters""" - async def initialise(self, controller: Any) -> None: - pass - async def put(self, attr: AttrW[T], value: T) -> None: await attr.update_display_without_process(value) @@ -107,7 +108,7 @@ def access_mode(self) -> AttrMode: def group(self) -> str | None: return self._group - async def initialise(self, controller: Any) -> None: + async def initialise(self, controller: fastcs.controller.BaseController) -> None: if self._handler is not None: await self._handler.initialise(controller) From 4423c2691f420022be5710f63202d5deeae0a6f8 Mon Sep 17 00:00:00 2001 From: Alan Greer Date: Wed, 26 Mar 2025 11:03:16 +0000 Subject: [PATCH 06/17] Removed unused controller instances from method signatures due to updated initialisation. --- src/fastcs/backend.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/fastcs/backend.py b/src/fastcs/backend.py index b37fb7ff2..dc566def8 100644 --- a/src/fastcs/backend.py +++ b/src/fastcs/backend.py @@ -32,7 +32,7 @@ def __init__( def _link_process_tasks(self): for controller_api in self.controller_api.walk_api(): _link_put_tasks(controller_api) - _link_attribute_sender_class(controller_api, self._controller) + _link_attribute_sender_class(controller_api) def __del__(self): self._stop_scan_tasks() @@ -75,9 +75,7 @@ def _link_put_tasks(controller_api: ControllerAPI) -> None: ) -def _link_attribute_sender_class( - controller_api: ControllerAPI, controller: Controller -) -> None: +def _link_attribute_sender_class(controller_api: ControllerAPI) -> None: for attr_name, attribute in controller_api.attributes.items(): match attribute: case AttrW(sender=Sender()): @@ -85,13 +83,13 @@ def _link_attribute_sender_class( f"Cannot assign both put method and Sender object to {attr_name}" ) - callback = _create_sender_callback(attribute, controller) + callback = _create_sender_callback(attribute) attribute.add_process_callback(callback) -def _create_sender_callback(attribute, controller): +def _create_sender_callback(attribute): async def callback(value): - await attribute.sender.put(controller, attribute, value) + await attribute.sender.put(attribute, value) return callback From 5b0513ab2361c4d2e8fb6dcb1d7aaf1ace9c788b Mon Sep 17 00:00:00 2001 From: Alan Greer Date: Wed, 26 Mar 2025 11:03:54 +0000 Subject: [PATCH 07/17] Updated handler_initialise test. --- tests/test_attribute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_attribute.py b/tests/test_attribute.py index b8bc683c8..179ae075c 100644 --- a/tests/test_attribute.py +++ b/tests/test_attribute.py @@ -69,7 +69,7 @@ async def test_handler_initialise(mocker: MockerFixture): await attr.initialise(mocker.ANY) # The handler initialise method should be called from the attribute - handler_mock.assert_called_once_with(1) + handler_mock.assert_called_once_with(mocker.ANY) @pytest.mark.parametrize( From cea619069b74a783a8481c8eee75b008b6d87a57 Mon Sep 17 00:00:00 2001 From: Alan Greer Date: Wed, 26 Mar 2025 14:32:44 +0000 Subject: [PATCH 08/17] Increase coverage. --- tests/test_attribute.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_attribute.py b/tests/test_attribute.py index 179ae075c..2c2350c44 100644 --- a/tests/test_attribute.py +++ b/tests/test_attribute.py @@ -71,6 +71,12 @@ async def test_handler_initialise(mocker: MockerFixture): # The handler initialise method should be called from the attribute handler_mock.assert_called_once_with(mocker.ANY) + handler = SimpleHandler() + attr = AttrW(Int(), handler=handler) + + # Assert no error in calling initialise on the SimpleHandler default + await attr.initialise(mocker.ANY) + @pytest.mark.parametrize( ["datatype", "init_args", "value"], From 0d1443901346431de9b139daa7a8b8d1bd7dcc8b Mon Sep 17 00:00:00 2001 From: Alan Greer Date: Wed, 26 Mar 2025 14:46:34 +0000 Subject: [PATCH 09/17] Increase coverage. --- tests/test_attribute.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_attribute.py b/tests/test_attribute.py index 2c2350c44..5dfbee4ec 100644 --- a/tests/test_attribute.py +++ b/tests/test_attribute.py @@ -4,7 +4,7 @@ import pytest from pytest_mock import MockerFixture -from fastcs.attributes import AttrR, AttrRW, AttrW, SimpleHandler +from fastcs.attributes import AttrR, AttrRW, AttrW, SimpleHandler, Updater from fastcs.datatypes import Enum, Float, Int, String, Waveform @@ -60,6 +60,10 @@ async def test_simple_handler_rw(mocker: MockerFixture): set_mock.assert_awaited_once_with(1) +class SimpleUpdater(Updater): + pass + + @pytest.mark.asyncio async def test_handler_initialise(mocker: MockerFixture): handler = SimpleHandler() @@ -77,6 +81,12 @@ async def test_handler_initialise(mocker: MockerFixture): # Assert no error in calling initialise on the SimpleHandler default await attr.initialise(mocker.ANY) + handler = SimpleUpdater() + attr = AttrR(Int(), handler=handler) + + # Assert no error in calling initialise on the TestUpdater handler + await attr.initialise(mocker.ANY) + @pytest.mark.parametrize( ["datatype", "init_args", "value"], From d2791a599368bcfb983a4059cfc0e33be9be8870 Mon Sep 17 00:00:00 2001 From: Alan Greer Date: Wed, 26 Mar 2025 16:00:42 +0000 Subject: [PATCH 10/17] Updated Handlers in assertable_controller test file to match signatures. --- tests/assertable_controller.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/assertable_controller.py b/tests/assertable_controller.py index 36f683cde..89934509d 100644 --- a/tests/assertable_controller.py +++ b/tests/assertable_controller.py @@ -15,13 +15,19 @@ class TestUpdater(Updater): update_period = 1 - async def update(self, controller, attr): - print(f"{controller} update {attr}") + async def initialise(self, controller) -> None: + self.controller = controller + + async def update(self, attr): + print(f"{self.controller} update {attr}") class TestSender(Sender): - async def put(self, controller, attr, value): - print(f"{controller}: {attr} = {value}") + async def initialise(self, controller) -> None: + self.controller = controller + + async def put(self, attr, value): + print(f"{self.controller}: {attr} = {value}") class TestHandler(Handler, TestUpdater, TestSender): @@ -47,6 +53,7 @@ def __init__(self) -> None: count = 0 async def initialise(self) -> None: + await super().initialise() self.initialised = True async def connect(self) -> None: From d29684c3dcf6e19d06dd5e9b3ce349d9cd6646ca Mon Sep 17 00:00:00 2001 From: Alan Greer Date: Wed, 2 Apr 2025 10:46:48 +0000 Subject: [PATCH 11/17] Renamed controller initialise to attribute_initialise and reinstate blank initialise method. --- src/fastcs/backend.py | 1 + src/fastcs/controller.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/fastcs/backend.py b/src/fastcs/backend.py index dc566def8..702c937cc 100644 --- a/src/fastcs/backend.py +++ b/src/fastcs/backend.py @@ -26,6 +26,7 @@ def __init__( # Initialise controller and then build its APIs loop.run_until_complete(controller.initialise()) + loop.run_until_complete(controller.attribute_initialise()) self.controller_api = build_controller_api(controller) self._link_process_tasks() diff --git a/src/fastcs/controller.py b/src/fastcs/controller.py index 368c01d2e..f074cccd5 100755 --- a/src/fastcs/controller.py +++ b/src/fastcs/controller.py @@ -30,7 +30,10 @@ def __init__( self._bind_attrs() - async def initialise(self) -> None: + async def initialise(self): + pass + + async def attribute_initialise(self) -> None: # Initialise any registered handlers for attributes coros = [attr.initialise(self) for attr in self.attributes.values()] try: From 36dfffb26aefb704211336a15cf61c946e599dc6 Mon Sep 17 00:00:00 2001 From: Alan Greer Date: Thu, 3 Apr 2025 16:09:48 +0000 Subject: [PATCH 12/17] Updated attribute handler test to use mock --- tests/test_attribute.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_attribute.py b/tests/test_attribute.py index 5dfbee4ec..2c4f960ee 100644 --- a/tests/test_attribute.py +++ b/tests/test_attribute.py @@ -70,10 +70,11 @@ async def test_handler_initialise(mocker: MockerFixture): handler_mock = mocker.patch.object(handler, "initialise") attr = AttrR(Int(), handler=handler) - await attr.initialise(mocker.ANY) + ctrlr = mocker.Mock() + await attr.initialise(ctrlr) # The handler initialise method should be called from the attribute - handler_mock.assert_called_once_with(mocker.ANY) + handler_mock.assert_called_once_with(ctrlr) handler = SimpleHandler() attr = AttrW(Int(), handler=handler) From e5298e6f09841b250611583d1351ad44b073a5ff Mon Sep 17 00:00:00 2001 From: Alan Greer Date: Fri, 25 Apr 2025 16:18:15 +0000 Subject: [PATCH 13/17] Removed Protocol from Handler classes. Renamed Sender to Setter. --- src/fastcs/attributes.py | 32 +++++++++++------------- src/fastcs/backend.py | 4 +-- tests/assertable_controller.py | 6 ++--- tests/conftest.py | 4 +-- tests/transport/epics/ca/test_softioc.py | 4 +-- tests/transport/graphQL/test_graphQL.py | 4 +-- tests/transport/rest/test_rest.py | 4 +-- tests/transport/tango/test_dsr.py | 4 +-- 8 files changed, 29 insertions(+), 33 deletions(-) diff --git a/src/fastcs/attributes.py b/src/fastcs/attributes.py index ccbb0cd7c..bff235d39 100644 --- a/src/fastcs/attributes.py +++ b/src/fastcs/attributes.py @@ -3,7 +3,7 @@ import asyncio from collections.abc import Callable from enum import Enum -from typing import Any, Generic, Protocol, runtime_checkable +from typing import Any, Generic import fastcs @@ -18,34 +18,30 @@ class AttrMode(Enum): READ_WRITE = 3 -@runtime_checkable -class Sender(Protocol): - """Protocol for setting the value of an ``Attribute``.""" - +class _BaseHandler: async def initialise(self, controller: fastcs.controller.BaseController) -> None: pass + +class Setter(_BaseHandler): + """Protocol for setting the value of an ``Attribute``.""" + async def put(self, attr: AttrW[T], value: T) -> None: pass -@runtime_checkable -class Updater(Protocol): +class Updater(_BaseHandler): """Protocol for updating the cached readback value of an ``Attribute``.""" # If update period is None then the attribute will not be updated as a task. update_period: float | None = None - async def initialise(self, controller: fastcs.controller.BaseController) -> None: - pass - async def update(self, attr: AttrR) -> None: pass -@runtime_checkable -class Handler(Sender, Updater, Protocol): - """Protocol encapsulating both ``Sender`` and ``Updater``.""" +class Handler(Setter, Updater): + """Protocol encapsulating both ``Setter`` and ``Updater``.""" pass @@ -179,7 +175,7 @@ def __init__( datatype: DataType[T], access_mode=AttrMode.WRITE, group: str | None = None, - handler: Sender | None = None, + handler: Setter | None = None, description: str | None = None, ) -> None: super().__init__( @@ -193,9 +189,9 @@ def __init__( self._write_display_callbacks: list[AttrCallback[T]] | None = None if handler is not None: - self._sender = handler + self._setter = handler else: - self._sender = SimpleHandler() + self._setter = SimpleHandler() async def process(self, value: T) -> None: await self.process_without_display_update(value) @@ -225,8 +221,8 @@ def add_write_display_callback(self, callback: AttrCallback[T]) -> None: self._write_display_callbacks.append(callback) @property - def sender(self) -> Sender: - return self._sender + def sender(self) -> Setter: + return self._setter class AttrRW(AttrR[T], AttrW[T]): diff --git a/src/fastcs/backend.py b/src/fastcs/backend.py index 702c937cc..6f0929153 100644 --- a/src/fastcs/backend.py +++ b/src/fastcs/backend.py @@ -4,7 +4,7 @@ from fastcs.cs_methods import Command, Put, Scan -from .attributes import AttrR, AttrW, Sender, Updater +from .attributes import AttrR, AttrW, Setter, Updater from .controller import BaseController, Controller from .controller_api import ControllerAPI from .exceptions import FastCSException @@ -79,7 +79,7 @@ def _link_put_tasks(controller_api: ControllerAPI) -> None: def _link_attribute_sender_class(controller_api: ControllerAPI) -> None: for attr_name, attribute in controller_api.attributes.items(): match attribute: - case AttrW(sender=Sender()): + case AttrW(sender=Setter()): assert not attribute.has_process_callback(), ( f"Cannot assign both put method and Sender object to {attr_name}" ) diff --git a/tests/assertable_controller.py b/tests/assertable_controller.py index 89934509d..f425ac2c8 100644 --- a/tests/assertable_controller.py +++ b/tests/assertable_controller.py @@ -4,7 +4,7 @@ from pytest_mock import MockerFixture, MockType -from fastcs.attributes import AttrR, Handler, Sender, Updater +from fastcs.attributes import AttrR, Handler, Setter, Updater from fastcs.backend import build_controller_api from fastcs.controller import Controller, SubController from fastcs.controller_api import ControllerAPI @@ -22,7 +22,7 @@ async def update(self, attr): print(f"{self.controller} update {attr}") -class TestSender(Sender): +class TestSetter(Setter): async def initialise(self, controller) -> None: self.controller = controller @@ -30,7 +30,7 @@ async def put(self, attr, value): print(f"{self.controller}: {attr} = {value}") -class TestHandler(Handler, TestUpdater, TestSender): +class TestHandler(Handler, TestUpdater, TestSetter): pass diff --git a/tests/conftest.py b/tests/conftest.py index 74feaa3e0..732fa44b8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ from tests.assertable_controller import ( MyTestController, TestHandler, - TestSender, + TestSetter, TestUpdater, ) from tests.example_p4p_ioc import run as _run_p4p_ioc @@ -37,7 +37,7 @@ class BackendTestController(MyTestController): read_write_int: AttrRW = AttrRW(Int(), handler=TestHandler()) read_write_float: AttrRW = AttrRW(Float()) read_bool: AttrR = AttrR(Bool()) - write_bool: AttrW = AttrW(Bool(), handler=TestSender()) + write_bool: AttrW = AttrW(Bool(), handler=TestSetter()) read_string: AttrRW = AttrRW(String()) diff --git a/tests/transport/epics/ca/test_softioc.py b/tests/transport/epics/ca/test_softioc.py index 4e73b517d..d240c6c70 100644 --- a/tests/transport/epics/ca/test_softioc.py +++ b/tests/transport/epics/ca/test_softioc.py @@ -8,7 +8,7 @@ AssertableControllerAPI, MyTestController, TestHandler, - TestSender, + TestSetter, TestUpdater, ) from tests.util import ColourEnum @@ -213,7 +213,7 @@ class EpicsController(MyTestController): read_write_int = AttrRW(Int(), handler=TestHandler()) read_write_float = AttrRW(Float()) read_bool = AttrR(Bool()) - write_bool = AttrW(Bool(), handler=TestSender()) + write_bool = AttrW(Bool(), handler=TestSetter()) 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/graphQL/test_graphQL.py b/tests/transport/graphQL/test_graphQL.py index c790b6869..76d03d27d 100644 --- a/tests/transport/graphQL/test_graphQL.py +++ b/tests/transport/graphQL/test_graphQL.py @@ -9,7 +9,7 @@ AssertableControllerAPI, MyTestController, TestHandler, - TestSender, + TestSetter, TestUpdater, ) @@ -23,7 +23,7 @@ class GraphQLController(MyTestController): read_write_int = AttrRW(Int(), handler=TestHandler()) read_write_float = AttrRW(Float()) read_bool = AttrR(Bool()) - write_bool = AttrW(Bool(), handler=TestSender()) + write_bool = AttrW(Bool(), handler=TestSetter()) read_string = AttrRW(String()) diff --git a/tests/transport/rest/test_rest.py b/tests/transport/rest/test_rest.py index 23b5fce99..79c16004a 100644 --- a/tests/transport/rest/test_rest.py +++ b/tests/transport/rest/test_rest.py @@ -8,7 +8,7 @@ AssertableControllerAPI, MyTestController, TestHandler, - TestSender, + TestSetter, TestUpdater, ) @@ -23,7 +23,7 @@ class RestController(MyTestController): read_write_int = AttrRW(Int(), handler=TestHandler()) read_write_float = AttrRW(Float()) read_bool = AttrR(Bool()) - write_bool = AttrW(Bool(), handler=TestSender()) + write_bool = AttrW(Bool(), handler=TestSetter()) 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 c38de8361..2e007dcb3 100644 --- a/tests/transport/tango/test_dsr.py +++ b/tests/transport/tango/test_dsr.py @@ -10,7 +10,7 @@ AssertableControllerAPI, MyTestController, TestHandler, - TestSender, + TestSetter, TestUpdater, ) @@ -37,7 +37,7 @@ class TangoController(MyTestController): read_write_int = AttrRW(Int(), handler=TestHandler()) read_write_float = AttrRW(Float()) read_bool = AttrR(Bool()) - write_bool = AttrW(Bool(), handler=TestSender()) + write_bool = AttrW(Bool(), handler=TestSetter()) read_string = AttrRW(String()) enum = AttrRW(Enum(enum.IntEnum("Enum", {"RED": 0, "GREEN": 1, "BLUE": 2}))) one_d_waveform = AttrRW(Waveform(np.int32, (10,))) From e7430a5304ece4fc4113d5851b82cc8c80e831df Mon Sep 17 00:00:00 2001 From: Alan Greer Date: Thu, 15 May 2025 16:01:36 +0000 Subject: [PATCH 14/17] Updated Handler names to be specific --- src/fastcs/attributes.py | 24 ++++++++++++------------ src/fastcs/backend.py | 6 +++--- tests/assertable_controller.py | 8 ++++---- tests/test_attribute.py | 8 ++++---- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/fastcs/attributes.py b/src/fastcs/attributes.py index bff235d39..8a3cd650b 100644 --- a/src/fastcs/attributes.py +++ b/src/fastcs/attributes.py @@ -18,19 +18,19 @@ class AttrMode(Enum): READ_WRITE = 3 -class _BaseHandler: +class _BaseAttrHandler: async def initialise(self, controller: fastcs.controller.BaseController) -> None: pass -class Setter(_BaseHandler): +class AttrHandlerW(_BaseAttrHandler): """Protocol for setting the value of an ``Attribute``.""" async def put(self, attr: AttrW[T], value: T) -> None: pass -class Updater(_BaseHandler): +class AttrHandlerR(_BaseAttrHandler): """Protocol for updating the cached readback value of an ``Attribute``.""" # If update period is None then the attribute will not be updated as a task. @@ -40,13 +40,13 @@ async def update(self, attr: AttrR) -> None: pass -class Handler(Setter, Updater): - """Protocol encapsulating both ``Setter`` and ``Updater``.""" +class AttrHandlerRW(AttrHandlerR, AttrHandlerW): + """Protocol encapsulating both ``AttrHandlerR`` and ``AttHandlerW``.""" pass -class SimpleHandler(Handler): +class SimpleAttrHandler(AttrHandlerRW): """Handler for internal parameters""" async def put(self, attr: AttrW[T], value: T) -> None: @@ -131,7 +131,7 @@ def __init__( datatype: DataType[T], access_mode=AttrMode.READ, group: str | None = None, - handler: Updater | None = None, + handler: AttrHandlerR | None = None, initial_value: T | None = None, description: str | None = None, ) -> None: @@ -163,7 +163,7 @@ def add_update_callback(self, callback: AttrCallback[T]) -> None: self._update_callbacks.append(callback) @property - def updater(self) -> Updater | None: + def updater(self) -> AttrHandlerR | None: return self._updater @@ -175,7 +175,7 @@ def __init__( datatype: DataType[T], access_mode=AttrMode.WRITE, group: str | None = None, - handler: Setter | None = None, + handler: AttrHandlerW | None = None, description: str | None = None, ) -> None: super().__init__( @@ -191,7 +191,7 @@ def __init__( if handler is not None: self._setter = handler else: - self._setter = SimpleHandler() + self._setter = SimpleAttrHandler() async def process(self, value: T) -> None: await self.process_without_display_update(value) @@ -221,7 +221,7 @@ def add_write_display_callback(self, callback: AttrCallback[T]) -> None: self._write_display_callbacks.append(callback) @property - def sender(self) -> Setter: + def sender(self) -> AttrHandlerW: return self._setter @@ -233,7 +233,7 @@ def __init__( datatype: DataType[T], access_mode=AttrMode.READ_WRITE, group: str | None = None, - handler: Handler | None = None, + handler: AttrHandlerRW | None = None, initial_value: T | None = None, description: str | None = None, ) -> None: diff --git a/src/fastcs/backend.py b/src/fastcs/backend.py index 6f0929153..5398dbc27 100644 --- a/src/fastcs/backend.py +++ b/src/fastcs/backend.py @@ -4,7 +4,7 @@ from fastcs.cs_methods import Command, Put, Scan -from .attributes import AttrR, AttrW, Setter, Updater +from .attributes import AttrHandlerR, AttrHandlerW, AttrR, AttrW from .controller import BaseController, Controller from .controller_api import ControllerAPI from .exceptions import FastCSException @@ -79,7 +79,7 @@ def _link_put_tasks(controller_api: ControllerAPI) -> None: def _link_attribute_sender_class(controller_api: ControllerAPI) -> None: for attr_name, attribute in controller_api.attributes.items(): match attribute: - case AttrW(sender=Setter()): + case AttrW(sender=AttrHandlerW()): assert not attribute.has_process_callback(), ( f"Cannot assign both put method and Sender object to {attr_name}" ) @@ -122,7 +122,7 @@ def _add_attribute_updater_tasks( ): for attribute in controller_api.attributes.values(): match attribute: - case AttrR(updater=Updater(update_period=update_period)) as attribute: + case AttrR(updater=AttrHandlerR(update_period=update_period)) as attribute: callback = _create_updater_callback(attribute, controller) if update_period is not None: scan_dict[update_period].append(callback) diff --git a/tests/assertable_controller.py b/tests/assertable_controller.py index f425ac2c8..5f9aa55b4 100644 --- a/tests/assertable_controller.py +++ b/tests/assertable_controller.py @@ -4,7 +4,7 @@ from pytest_mock import MockerFixture, MockType -from fastcs.attributes import AttrR, Handler, Setter, Updater +from fastcs.attributes import AttrHandlerR, AttrHandlerRW, AttrHandlerW, AttrR from fastcs.backend import build_controller_api from fastcs.controller import Controller, SubController from fastcs.controller_api import ControllerAPI @@ -12,7 +12,7 @@ from fastcs.wrappers import command, scan -class TestUpdater(Updater): +class TestUpdater(AttrHandlerR): update_period = 1 async def initialise(self, controller) -> None: @@ -22,7 +22,7 @@ async def update(self, attr): print(f"{self.controller} update {attr}") -class TestSetter(Setter): +class TestSetter(AttrHandlerW): async def initialise(self, controller) -> None: self.controller = controller @@ -30,7 +30,7 @@ async def put(self, attr, value): print(f"{self.controller}: {attr} = {value}") -class TestHandler(Handler, TestUpdater, TestSetter): +class TestHandler(AttrHandlerRW, TestUpdater, TestSetter): pass diff --git a/tests/test_attribute.py b/tests/test_attribute.py index 2c4f960ee..2a8e788c2 100644 --- a/tests/test_attribute.py +++ b/tests/test_attribute.py @@ -4,7 +4,7 @@ import pytest from pytest_mock import MockerFixture -from fastcs.attributes import AttrR, AttrRW, AttrW, SimpleHandler, Updater +from fastcs.attributes import AttrHandlerR, AttrHandlerRW, AttrR, AttrRW, AttrW from fastcs.datatypes import Enum, Float, Int, String, Waveform @@ -60,13 +60,13 @@ async def test_simple_handler_rw(mocker: MockerFixture): set_mock.assert_awaited_once_with(1) -class SimpleUpdater(Updater): +class SimpleUpdater(AttrHandlerR): pass @pytest.mark.asyncio async def test_handler_initialise(mocker: MockerFixture): - handler = SimpleHandler() + handler = AttrHandlerRW() handler_mock = mocker.patch.object(handler, "initialise") attr = AttrR(Int(), handler=handler) @@ -76,7 +76,7 @@ async def test_handler_initialise(mocker: MockerFixture): # The handler initialise method should be called from the attribute handler_mock.assert_called_once_with(ctrlr) - handler = SimpleHandler() + handler = AttrHandlerRW() attr = AttrW(Int(), handler=handler) # Assert no error in calling initialise on the SimpleHandler default From 89dd9eb74249c9ac91cfbbf594bb3daa49fcdffb Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Fri, 16 May 2025 10:22:35 +0000 Subject: [PATCH 15/17] Fix updater callbacks --- src/fastcs/attributes.py | 2 +- src/fastcs/backend.py | 27 ++++++++++++--------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/fastcs/attributes.py b/src/fastcs/attributes.py index 8a3cd650b..ab0494e89 100644 --- a/src/fastcs/attributes.py +++ b/src/fastcs/attributes.py @@ -36,7 +36,7 @@ class AttrHandlerR(_BaseAttrHandler): # If update period is None then the attribute will not be updated as a task. update_period: float | None = None - async def update(self, attr: AttrR) -> None: + async def update(self, attr: AttrR[T]) -> None: pass diff --git a/src/fastcs/backend.py b/src/fastcs/backend.py index 5398dbc27..cc2bf8c5f 100644 --- a/src/fastcs/backend.py +++ b/src/fastcs/backend.py @@ -3,6 +3,7 @@ from collections.abc import Callable from fastcs.cs_methods import Command, Put, Scan +from fastcs.datatypes import T from .attributes import AttrHandlerR, AttrHandlerW, AttrR, AttrW from .controller import BaseController, Controller @@ -49,7 +50,7 @@ async def _run_initial_coros(self): async def _start_scan_tasks(self): self._scan_tasks = { self._loop.create_task(coro()) - for coro in _get_scan_coros(self.controller_api, self._controller) + for coro in _get_scan_coros(self.controller_api) } def _stop_scan_tasks(self): @@ -95,14 +96,12 @@ async def callback(value): return callback -def _get_scan_coros( - root_controller_api: ControllerAPI, controller: Controller -) -> list[Callable]: +def _get_scan_coros(root_controller_api: ControllerAPI) -> list[Callable]: scan_dict: dict[float, list[Callable]] = defaultdict(list) for controller_api in root_controller_api.walk_api(): _add_scan_method_tasks(scan_dict, controller_api) - _add_attribute_updater_tasks(scan_dict, controller_api, controller) + _add_attribute_updater_tasks(scan_dict, controller_api) scan_coros = _get_periodic_scan_coros(scan_dict) return scan_coros @@ -116,27 +115,25 @@ def _add_scan_method_tasks( def _add_attribute_updater_tasks( - scan_dict: dict[float, list[Callable]], - controller_api: ControllerAPI, - controller: Controller, + scan_dict: dict[float, list[Callable]], controller_api: ControllerAPI ): for attribute in controller_api.attributes.values(): match attribute: case AttrR(updater=AttrHandlerR(update_period=update_period)) as attribute: - callback = _create_updater_callback(attribute, controller) + callback = _create_updater_callback(attribute) if update_period is not None: scan_dict[update_period].append(callback) -def _create_updater_callback(attribute, controller): +def _create_updater_callback(attribute: AttrR[T]): + updater = attribute.updater + assert updater is not None + async def callback(): try: - await attribute.updater.update(controller, attribute) + await updater.update(attribute) except Exception as e: - print( - f"Update loop in {attribute.updater} stopped:\n" - f"{e.__class__.__name__}: {e}" - ) + print(f"Update loop in {updater} stopped:\n{e.__class__.__name__}: {e}") raise return callback From 1ebb3e69f03c21090e9c9c4941d87f1dcf1e3787 Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Fri, 16 May 2025 11:24:42 +0000 Subject: [PATCH 16/17] Initialise sub controllers recursively --- src/fastcs/controller.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/fastcs/controller.py b/src/fastcs/controller.py index f074cccd5..88cbc92f5 100755 --- a/src/fastcs/controller.py +++ b/src/fastcs/controller.py @@ -41,6 +41,9 @@ async def attribute_initialise(self) -> None: except asyncio.CancelledError: pass + for controller in self.get_sub_controllers().values(): + await controller.attribute_initialise() + @property def path(self) -> list[str]: """Path prefix of attributes, recursively including parent Controllers.""" From d52f300713b5a397bdc7b39bbce53e05a1188d59 Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Fri, 16 May 2025 11:25:22 +0000 Subject: [PATCH 17/17] Deep copy attributes when building to make handlers unique --- src/fastcs/controller.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/fastcs/controller.py b/src/fastcs/controller.py index 88cbc92f5..c01380003 100755 --- a/src/fastcs/controller.py +++ b/src/fastcs/controller.py @@ -1,7 +1,7 @@ from __future__ import annotations import asyncio -from copy import copy +from copy import deepcopy from typing import get_type_hints from fastcs.attributes import Attribute @@ -91,7 +91,8 @@ class method and a controller instance, so that it can be called from any f"`{type(self).__name__}` has conflicting attribute " f"`{attr_name}` already present in the attributes dict." ) - new_attribute = copy(attr) + + new_attribute = deepcopy(attr) setattr(self, attr_name, new_attribute) self.attributes[attr_name] = new_attribute elif isinstance(attr, UnboundPut | UnboundScan | UnboundCommand):