From f8423846674474b0b6e1c8d3753bd85d7e164c7f Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Wed, 3 Dec 2025 13:55:20 +0000 Subject: [PATCH] Add AttributeInfo to apply metadata to hinted Attributes --- src/fastcs/attributes/__init__.py | 1 + src/fastcs/attributes/attribute.py | 10 +++++- src/fastcs/attributes/attribute_info.py | 9 +++++ src/fastcs/attributes/hinted_attribute.py | 2 ++ src/fastcs/controllers/base_controller.py | 42 ++++++++++++++++++----- 5 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 src/fastcs/attributes/attribute_info.py diff --git a/src/fastcs/attributes/__init__.py b/src/fastcs/attributes/__init__.py index b25e66f72..39de988d7 100644 --- a/src/fastcs/attributes/__init__.py +++ b/src/fastcs/attributes/__init__.py @@ -2,6 +2,7 @@ from .attr_rw import AttrRW as AttrRW from .attr_w import AttrW as AttrW from .attribute import Attribute as Attribute +from .attribute_info import AttributeInfo as AttributeInfo from .attribute_io import AnyAttributeIO as AnyAttributeIO from .attribute_io import AttributeIO as AttributeIO from .attribute_io_ref import AttributeIORef as AttributeIORef diff --git a/src/fastcs/attributes/attribute.py b/src/fastcs/attributes/attribute.py index 546dab165..e62a6ff9f 100644 --- a/src/fastcs/attributes/attribute.py +++ b/src/fastcs/attributes/attribute.py @@ -1,7 +1,7 @@ from collections.abc import Callable from typing import Generic -from fastcs.attributes.attribute_io_ref import AttributeIORefT +from fastcs.attributes import AttributeInfo, AttributeIORefT from fastcs.datatypes import DataType, DType, DType_T from fastcs.logging import bind_logger from fastcs.tracer import Tracer @@ -100,6 +100,14 @@ def set_path(self, path: list[str]): self._path = path + def add_info(self, info: AttributeInfo): + """Apply info fields""" + if info.description: + self.description = info.description + + if info.group: + self._group = info.group + def __repr__(self): name = self.__class__.__name__ path = ".".join(self._path + [self._name]) or None diff --git a/src/fastcs/attributes/attribute_info.py b/src/fastcs/attributes/attribute_info.py new file mode 100644 index 000000000..ae6a881c7 --- /dev/null +++ b/src/fastcs/attributes/attribute_info.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + + +@dataclass(kw_only=True) +class AttributeInfo: + """Fields to apply to hinted attributes during introspection""" + + description: str | None = None + group: str | None = None diff --git a/src/fastcs/attributes/hinted_attribute.py b/src/fastcs/attributes/hinted_attribute.py index d58fadd46..dac550228 100644 --- a/src/fastcs/attributes/hinted_attribute.py +++ b/src/fastcs/attributes/hinted_attribute.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from fastcs.attributes.attribute import Attribute +from fastcs.attributes.attribute_info import AttributeInfo from fastcs.datatypes import DType @@ -16,3 +17,4 @@ class HintedAttribute: """The type of the `Attribute` in the type hint - e.g. `AttrR`""" dtype: type[DType] | None """The dtype of the `Attribute` in the type hint, if any - e.g. `int`""" + info: AttributeInfo | None diff --git a/src/fastcs/controllers/base_controller.py b/src/fastcs/controllers/base_controller.py index ff7507df4..c737666ec 100755 --- a/src/fastcs/controllers/base_controller.py +++ b/src/fastcs/controllers/base_controller.py @@ -3,10 +3,17 @@ from collections import Counter from collections.abc import Sequence from copy import deepcopy -from typing import _GenericAlias, get_args, get_origin, get_type_hints # type: ignore +from typing import ( + Annotated, + _GenericAlias, # type: ignore + get_args, + get_origin, + get_type_hints, +) from fastcs.attributes import ( Attribute, + AttributeInfo, AttributeIO, AttributeIORefT, AttrR, @@ -67,7 +74,13 @@ def __init__( def _find_type_hints(self): """Find `Attribute` and `Controller` type hints for introspection validation""" - for name, hint in get_type_hints(type(self)).items(): + for name, hint in get_type_hints(type(self), include_extras=True).items(): + # Annotated[AttrR[int], AttributeInfo(...)] + metadata = None + if isinstance(origin := get_origin(hint), type) and origin is Annotated: + args = get_args(hint) + hint, metadata = args[0], args[1:] + if isinstance(hint, _GenericAlias): # e.g. AttrR[int] args = get_args(hint) hint = get_origin(hint) @@ -78,16 +91,25 @@ def _find_type_hints(self): if args is None: dtype = None else: - if len(args) == 2: - dtype = args[0] - else: + if len(args) != 2: raise TypeError( f"Invalid type hint for attribute {name}: {hint}" ) - self.__hinted_attributes[name] = HintedAttribute( - attr_type=hint, dtype=dtype - ) + dtype, _io_ref = args + if metadata is not None: + if not isinstance(metadata[0], AttributeInfo): + raise TypeError( + f"Invalid annotation for attribute {name}: {hint}" + ) + else: + info = metadata[0] + else: + info = None + + self.__hinted_attributes[name] = HintedAttribute( + attr_type=origin, dtype=dtype, info=info + ) elif isinstance(hint, type) and issubclass(hint, BaseController): self.__hinted_sub_controllers[name] = hint @@ -259,6 +281,10 @@ def add_attribute(self, name, attr: Attribute): f"Expected '{hint.dtype.__name__}', " f"got '{attr.datatype.dtype.__name__}'." ) + + if hint.info is not None: + attr.add_info(hint.info) + elif name in self.__sub_controllers.keys(): raise ValueError( f"Cannot add attribute {attr}. "