Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/fastcs/controllers/controller_vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
from fastcs.attributes import AnyAttributeIO
from fastcs.controllers.base_controller import BaseController
from fastcs.controllers.controller import Controller
from fastcs.util import Controller_T


class ControllerVector(MutableMapping[int, Controller], BaseController):
class ControllerVector(MutableMapping[int, Controller_T], BaseController):
"""Controller containing Attributes and indexed sub Controllers

The sub controllers registered with this Controller should be instances of the same
Expand All @@ -15,12 +16,12 @@ class ControllerVector(MutableMapping[int, Controller], BaseController):

def __init__(
self,
children: Mapping[int, Controller],
children: Mapping[int, Controller_T],
description: str | None = None,
ios: Sequence[AnyAttributeIO] | None = None,
) -> None:
super().__init__(description=description, ios=ios)
self._children: dict[int, Controller] = {}
self._children: dict[int, Controller_T] = {}
for index, child in children.items():
self[index] = child

Expand All @@ -31,15 +32,15 @@ def add_sub_controller(self, name: str, sub_controller: BaseController):
"E.g., vector[1] = Controller()"
)

def __getitem__(self, key: int) -> Controller:
def __getitem__(self, key: int) -> Controller_T:
try:
return self._children[key]
except KeyError as exception:
raise KeyError(
f"ControllerVector does not have Controller with key {key}"
) from exception

def __setitem__(self, key: int, value: Controller) -> None:
def __setitem__(self, key: int, value: Controller_T) -> None:
if not isinstance(key, int):
msg = f"Expected int, got {key}"
raise TypeError(msg)
Expand Down
8 changes: 2 additions & 6 deletions src/fastcs/methods/method.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
from asyncio import iscoroutinefunction
from collections.abc import Callable, Coroutine
from inspect import Signature, getdoc, signature
from typing import TYPE_CHECKING, Generic, TypeVar
from typing import Generic

from fastcs.tracer import Tracer

if TYPE_CHECKING:
from fastcs.controllers import BaseController # noqa: F401
from fastcs.util import Controller_T

MethodCallback = Callable[..., Coroutine[None, None, None]]
"""Generic protocol for all `Controller` Method callbacks"""
Controller_T = TypeVar("Controller_T", bound="BaseController") # noqa: F821
"""Generic `Controller` class that an unbound method must be called with as `self`"""


class Method(Generic[Controller_T], Tracer):
Expand Down
7 changes: 7 additions & 0 deletions src/fastcs/util.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import re
from typing import TYPE_CHECKING, TypeVar

if TYPE_CHECKING:
from fastcs.controllers import BaseController # noqa: F401

Controller_T = TypeVar("Controller_T", bound="BaseController") # noqa: F821
"""Generic `Controller` class that an unbound method must be called with as `self`"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason not to define this in base_controller.py?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's used in methods and controllers, I thought util was most sensible, but I guess methods importing from controllers is not that strange; is this a nit?

Copy link
Contributor

@GDYendell GDYendell Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not even a nit. I am not sure what is best.

util.py is kind of intended to be a place that everything can import from. If it itself needs to import from the modules then that breaks. You couldn't import util from the controllers module now because it won't be fully initialised, although because of the if TYPE_CHECKING it is OK

I think it makes sense to put it in controllers somewhere. It could even be defined in __init__.py.

Would you prefer where it is currently?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I agree with you, utils importing from a sub-directory is not ideal; I liked the idea of putting Controller_T in controllers __init__, but this makes it partially resolved for methods, which forces it into the if TYPE_CHECKING condition, which is no good, as we need it to exist at runtime. Keeping it in utils, although not an intended use of a utils file, lets us grab Controller_T in both methods and controllers at runtime, and type check Controller_T during linting due to the IF TYPE_CHECKING condition on BaseController. Although, this would break if we started enforcing import contracts in the future... What do you think?


ONCE = float("inf")
"""Sentinel value to call a ``scan`` or io ``update`` method once on start up"""
Expand Down