From 603fbc44102cf3cd73fe6068a4a25e5e0e1cde47 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Mon, 17 Mar 2025 11:53:32 +0000 Subject: [PATCH 1/3] Add walk attributes, methods, and unit tests for controller --- src/fastcs/controller.py | 16 ++++++++++ tests/test_controller.py | 66 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/fastcs/controller.py b/src/fastcs/controller.py index c7fbc45fa..77ae8b18e 100755 --- a/src/fastcs/controller.py +++ b/src/fastcs/controller.py @@ -120,6 +120,22 @@ async def initialise(self) -> None: async def connect(self) -> None: pass + def walk_attributes(self, access_mode: type[Attribute]): + return { + attr: values + for attr, values in self.attributes.items() + if isinstance(values, access_mode) + } + + def walk_methods(self, access_mode: type[Attribute]): + return list( + filter( + lambda func: not func.startswith("__") + and callable(getattr(access_mode, func)), + dir(access_mode), + ) + ) + class SubController(BaseController): """A subordinate to a ``Controller`` for managing a subset of a device. diff --git a/tests/test_controller.py b/tests/test_controller.py index b0e87fd07..8ce027098 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -1,6 +1,6 @@ import pytest -from fastcs.attributes import AttrR +from fastcs.attributes import AttrR, AttrRW, AttrW from fastcs.controller import Controller, SubController from fastcs.datatypes import Int @@ -43,6 +43,8 @@ class SomeController(Controller): annotated_attr_not_defined_in_init: AttrR[int] equal_attr = AttrR(Int()) annotated_and_equal_attr: AttrR[int] = AttrR(Int()) + read_write_attr = AttrRW(Int()) + write_only_attr = AttrW(Int()) def __init__(self, sub_controller: SubController): self.attributes = {} @@ -67,6 +69,8 @@ def test_attribute_parsing(): "_attributes_attr_equal", "annotated_and_equal_attr", "equal_attr", + "read_write_attr", + "write_only_attr", "sub_controller", } @@ -112,3 +116,63 @@ class FailingController(SomeController): ), ): FailingController(SomeSubController()) + + +def test_walk_attributes_for_type(): + sub_controller = SomeSubController() + controller = SomeController(sub_controller) + + assert set(controller.walk_attributes(access_mode=AttrR).keys()) == { + "_attributes_attr", + "annotated_attr", + "_attributes_attr_equal", + "annotated_and_equal_attr", + "equal_attr", + "read_write_attr", + "sub_controller", + } + + assert set(controller.walk_attributes(access_mode=AttrW).keys()) == { + "write_only_attr", + "read_write_attr", + } + + pass + + +def test_walk_methods_for_type(): + sub_controller = SomeSubController() + controller = SomeController(sub_controller) + + assert set(controller.walk_methods(access_mode=AttrR)) == { + "add_update_callback", + "add_update_datatype_callback", + "get", + "set", + "update_datatype", + } + + assert set(controller.walk_methods(access_mode=AttrRW)) == { + "add_process_callback", + "add_update_callback", + "add_update_datatype_callback", + "add_write_display_callback", + "get", + "has_process_callback", + "process", + "process_without_display_update", + "set", + "update_datatype", + "update_display_without_process", + } + + assert set(controller.walk_methods(access_mode=AttrW)) == { + "add_process_callback", + "add_update_datatype_callback", + "add_write_display_callback", + "has_process_callback", + "process", + "process_without_display_update", + "update_datatype", + "update_display_without_process", + } From 86679e9d1ff6e689e87cf40bd4946c593256126b Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Mon, 17 Mar 2025 18:05:31 +0000 Subject: [PATCH 2/3] Apply review comments and add proxy for method --- src/fastcs/controller.py | 56 ++++++++++++++++++++++++++--------- tests/test_controller.py | 63 ++++++++++++++++++---------------------- 2 files changed, 72 insertions(+), 47 deletions(-) diff --git a/src/fastcs/controller.py b/src/fastcs/controller.py index 77ae8b18e..691f39ff1 100755 --- a/src/fastcs/controller.py +++ b/src/fastcs/controller.py @@ -1,11 +1,37 @@ from __future__ import annotations +from collections.abc import Callable, Coroutine from copy import copy -from typing import get_type_hints +from inspect import Parameter +from types import MappingProxyType +from typing import Any, Protocol, TypeVar, get_type_hints from fastcs.attributes import Attribute +class MethodProtocol(Protocol): + """Protocol for FastCS Controller methods""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: ... + + def _validate(self, fn: Callable[..., Coroutine[Any, Any, None]]) -> None: ... + + @property + def return_type(self) -> Any: ... + + @property + def parameters(self) -> MappingProxyType[str, Parameter]: ... + + @property + def docstring(self) -> str | None: ... + + @property + def fn(self) -> Callable[..., Coroutine[Any, Any, None]]: ... + + @property + def group(self) -> str | None: ... + + class BaseController: #: Attributes passed from the device at runtime. attributes: dict[str, Attribute] @@ -102,6 +128,10 @@ def get_sub_controllers(self) -> dict[str, SubController]: return self.__sub_controller_tree +Attribute_T = TypeVar("Attribute_T", bound=Attribute) +Method_T = TypeVar("Method_T", bound=MethodProtocol) + + class Controller(BaseController): """Top-level controller for a device. @@ -120,21 +150,21 @@ async def initialise(self) -> None: async def connect(self) -> None: pass - def walk_attributes(self, access_mode: type[Attribute]): + def walk_attributes( + self, access_mode: type[Attribute_T] = Attribute + ) -> dict[str, Attribute_T]: return { - attr: values - for attr, values in self.attributes.items() - if isinstance(values, access_mode) + name: attribute + for name, attribute in self.attributes.items() + if isinstance(attribute, access_mode) } - def walk_methods(self, access_mode: type[Attribute]): - return list( - filter( - lambda func: not func.startswith("__") - and callable(getattr(access_mode, func)), - dir(access_mode), - ) - ) + def walk_methods(self, method: type[Method_T]) -> dict[str, Method_T]: + return { + attribute: getattr(self, attribute) + for attribute in dir(self) + if isinstance(getattr(self, attribute), method) + } class SubController(BaseController): diff --git a/tests/test_controller.py b/tests/test_controller.py index 8ce027098..2f3793153 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -2,7 +2,9 @@ from fastcs.attributes import AttrR, AttrRW, AttrW from fastcs.controller import Controller, SubController +from fastcs.cs_methods import Command, Put, Scan from fastcs.datatypes import Int +from fastcs.wrappers import command, put, scan def test_controller_nesting(): @@ -58,6 +60,18 @@ def __init__(self, sub_controller: SubController): super().__init__() self.register_sub_controller("sub_controller", sub_controller) + @command() + async def test_command(self): + pass + + @scan(period=1.0) + async def test_scan(self): + pass + + @put + async def test_put(self, fn): + pass + def test_attribute_parsing(): sub_controller = SomeSubController() @@ -118,7 +132,7 @@ class FailingController(SomeController): FailingController(SomeSubController()) -def test_walk_attributes_for_type(): +def test_walk_attributes_from_type(): sub_controller = SomeSubController() controller = SomeController(sub_controller) @@ -140,39 +154,20 @@ def test_walk_attributes_for_type(): pass -def test_walk_methods_for_type(): +@pytest.mark.asyncio +@pytest.mark.parametrize( + "method_type, expected_methods", + [ + (Command, {"test_command"}), + (Scan, {"test_scan"}), + (Put, {"test_put"}), + ], +) +async def test_walk_methods_from_type(method_type, expected_methods): sub_controller = SomeSubController() controller = SomeController(sub_controller) - assert set(controller.walk_methods(access_mode=AttrR)) == { - "add_update_callback", - "add_update_datatype_callback", - "get", - "set", - "update_datatype", - } - - assert set(controller.walk_methods(access_mode=AttrRW)) == { - "add_process_callback", - "add_update_callback", - "add_update_datatype_callback", - "add_write_display_callback", - "get", - "has_process_callback", - "process", - "process_without_display_update", - "set", - "update_datatype", - "update_display_without_process", - } - - assert set(controller.walk_methods(access_mode=AttrW)) == { - "add_process_callback", - "add_update_datatype_callback", - "add_write_display_callback", - "has_process_callback", - "process", - "process_without_display_update", - "update_datatype", - "update_display_without_process", - } + methods = set(controller.walk_methods(method_type)) + assert len(methods) == len(expected_methods) + assert methods == expected_methods + methods = set(controller.walk_methods(Command)) From 31e97814b7d801a57b126d716a3add9cbb6af212 Mon Sep 17 00:00:00 2001 From: Shihab Suliman Date: Tue, 18 Mar 2025 09:27:38 +0000 Subject: [PATCH 3/3] Use walrus operator instead of getattr twice --- src/fastcs/controller.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fastcs/controller.py b/src/fastcs/controller.py index 691f39ff1..ca549b4e1 100755 --- a/src/fastcs/controller.py +++ b/src/fastcs/controller.py @@ -161,9 +161,9 @@ def walk_attributes( def walk_methods(self, method: type[Method_T]) -> dict[str, Method_T]: return { - attribute: getattr(self, attribute) - for attribute in dir(self) - if isinstance(getattr(self, attribute), method) + attr: value + for attr in dir(self) + if isinstance(value := getattr(self, attr), method) }