Skip to content

Commit

Permalink
Added root_attribute
Browse files Browse the repository at this point in the history
Added a method to `SubController` to allow passing of a top level
`Attribute` from the `SubController` to the parent controller.

The `Attribute` has the same name as the sub controller in the parent
controller.
  • Loading branch information
evalott100 committed Nov 26, 2024
1 parent 93af3e4 commit 779073f
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 24 deletions.
10 changes: 8 additions & 2 deletions src/fastcs/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class BaseController:
def __init__(self, path: list[str] | None = None) -> None:
self._path: list[str] = path or []
self.__sub_controller_tree: dict[str, BaseController] = {}
self.__sub_controller_tree: dict[str, SubController] = {}

self._bind_attrs()

Expand All @@ -25,6 +25,8 @@ def set_path(self, path: list[str]):

def _bind_attrs(self) -> None:
for attr_name in dir(self):
if attr_name == "root_attribute":
continue
attr = getattr(self, attr_name)
if isinstance(attr, Attribute):
new_attribute = copy(attr)
Expand All @@ -39,7 +41,7 @@ def register_sub_controller(self, name: str, sub_controller: SubController):
self.__sub_controller_tree[name] = sub_controller
sub_controller.set_path(self.path + [name])

def get_sub_controllers(self) -> dict[str, BaseController]:
def get_sub_controllers(self) -> dict[str, SubController]:
return self.__sub_controller_tree

def get_attributes(self) -> dict[str, Attribute]:
Expand Down Expand Up @@ -75,3 +77,7 @@ class SubController(BaseController):

def __init__(self) -> None:
super().__init__()

@property
def root_attribute(self) -> Attribute | None:
return None
25 changes: 20 additions & 5 deletions src/fastcs/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from dataclasses import dataclass
from typing import get_type_hints

from .attributes import Attribute, AttrR, AttrW, AttrRW
from .attributes import Attribute, AttrR, AttrRW, AttrW
from .controller import BaseController, Controller
from .cs_methods import Command, Put, Scan
from .wrappers import WrappedMethod
Expand Down Expand Up @@ -54,6 +54,9 @@ def _get_single_mapping(controller: BaseController) -> SingleMapping:

attributes: dict[str, Attribute] = {}
for name in list(get_type_hints(type(controller))) + dir(type(controller)):
if name == "root_attribute":
continue

if (
isinstance(
(attr := getattr(controller, name, None)), AttrRW | AttrR | AttrW
Expand All @@ -71,14 +74,26 @@ def _get_single_mapping(controller: BaseController) -> SingleMapping:
for key in object_defined_attributes.keys() & attributes.keys()
if object_defined_attributes[key] is not attributes[key]
}:
raise TypeError(
f"{controller} has conflicting attributes between those passed in"
"`get_attributes` and those obtained from the class definition: "
f"{conflicting_keys}"
raise ValueError(
f"`{type(controller).__name__}` has conflicting attributes between "
"those passed in `get_attributes` and those obtained from the "
f"class definition: {conflicting_keys}"
)

attributes.update(object_defined_attributes)

for sub_controller_name, sub_controller in controller.get_sub_controllers().items():
root_attribute = sub_controller.root_attribute
if root_attribute is None:
continue

if sub_controller_name in attributes:
raise ValueError(
f"sub_controller `{sub_controller_name}` has a `root_attribute` "
f"already defined defined in parent controller {sub_controller_name}"
)
attributes[sub_controller_name] = root_attribute

return SingleMapping(
controller, scan_methods, put_methods, command_methods, attributes
)
2 changes: 1 addition & 1 deletion tests/backends/epics/test_ioc.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, get_type_hints
from typing import Any

import pytest
from pytest_mock import MockerFixture
Expand Down
97 changes: 81 additions & 16 deletions tests/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,33 +33,98 @@ def test_controller_nesting():
controller.register_sub_controller("c", sub_controller)


def test_attribute_parsing():
runtime_attribute = AttrR(Int())
class SomeSubController(SubController):
def __init__(self):
self._root_attribute = AttrR(Int())
super().__init__()

class SomeController(Controller):
annotated_attr = AttrR(Int())
annotated_attr_not_defined_in_init: AttrR[int]
equal_attr = AttrR(Int())
annotated_and_equal_attr: AttrR[int] = AttrR(Int())
sub_attribute = AttrR(Int())

def get_attributes(self) -> dict[str, Attribute]:
return {"get_attributes_attr": runtime_attribute}
@property
def root_attribute(self):
return self._root_attribute

def __init__(self):
self.annotated_attr = AttrR(Int())
super().__init__()

controller = SomeController()
mapping = next(_walk_mappings(controller))
assert mapping.attributes == {
"get_attributes_attr": runtime_attribute,
class SomeController(Controller):
annotated_attr = AttrR(Int())
annotated_attr_not_defined_in_init: AttrR[int]
equal_attr = AttrR(Int())
annotated_and_equal_attr: AttrR[int] = AttrR(Int())

def __init__(self, sub_controller: SubController):
self.get_attributes_attribute = AttrR(Int())
self.annotated_attr = AttrR(Int())
self.attr_not_walked = AttrR(Int())

super().__init__()

self.register_sub_controller("sub_controller", sub_controller)

def get_attributes(self) -> dict[str, Attribute]:
return {
"get_attributes_attr": self.get_attributes_attribute,
"equal_attr": self.equal_attr,
}


def test_attribute_parsing():
sub_controller = SomeSubController()
controller = SomeController(sub_controller)

mapping_walk = _walk_mappings(controller)

controller_mapping = next(mapping_walk)
assert controller_mapping.attributes == {
"get_attributes_attr": controller.get_attributes_attribute,
"annotated_attr": controller.annotated_attr,
"equal_attr": controller.equal_attr,
"annotated_and_equal_attr": controller.annotated_and_equal_attr,
"sub_controller": sub_controller.root_attribute,
}

assert SomeController.equal_attr is not controller.equal_attr
assert (
SomeController.annotated_and_equal_attr
is not controller.annotated_and_equal_attr
)

sub_controller_mapping = next(mapping_walk)
assert sub_controller_mapping.attributes == {
"sub_attribute": sub_controller.sub_attribute,
}


def test_root_attribute():
class FailingController(SomeController):
def get_attributes(self) -> dict[str, Attribute]:
return {"sub_controller": self.get_attributes_attribute}

with pytest.raises(
ValueError,
match=(
"sub_controller `sub_controller` has a `root_attribute` already "
"defined defined in parent controller sub_controller"
),
):
next(_walk_mappings(FailingController(SomeSubController())))


def test_attribute_in_both_class_and_get_attributes():
class FailingController(Controller):
duplicate_attribute = AttrR(Int())

def __init__(self):
super().__init__()

def get_attributes(self) -> dict[str, Attribute]:
return {"duplicate_attribute": AttrR(Int())}

with pytest.raises(
ValueError,
match=(
"`FailingController` has conflicting attributes between those passed "
"in `get_attributes` and those obtained from the class definition: "
"{'duplicate_attribute'}"
),
):
next(_walk_mappings(FailingController()))

0 comments on commit 779073f

Please sign in to comment.