Source code for fastcs.methods.scan

from collections.abc import Callable, Coroutine
from types import MethodType
from typing import TYPE_CHECKING

from fastcs.logging import bind_logger
from fastcs.methods.method import Controller_T, Method

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

logger = bind_logger(logger_name=__name__)

UnboundScanCallback = Callable[[Controller_T], Coroutine[None, None, None]]
"""A Scan callback that is unbound and must be called with a `Controller` instance"""
ScanCallback = Callable[[], Coroutine[None, None, None]]
"""A Scan callback that is bound and can be called without `self`"""


[docs] class Scan(Method["BaseController"]): """A `Controller` `Method` that will be called periodically in the background. This class contains a function that is bound to a specific `Controller` instance and is callable outside of the class context, without an explicit `self` parameter. Calling an instance of this class will call the bound `Controller` method. """ def __init__(self, fn: ScanCallback, period: float): super().__init__(fn) self._period = period @property def period(self): return self._period def _validate(self, fn: ScanCallback) -> None: super()._validate(fn) if not len(self.parameters) == 0: raise TypeError("Scan method cannot have arguments") async def __call__(self): return await self._fn() @property def fn(self) -> ScanCallback: async def scan(): try: return await self._fn() except Exception: logger.error("Scan update loop stopped", fn=self._fn) raise return scan
[docs] class UnboundScan(Method[Controller_T]): """A wrapper of an unbound `Controller` method to be bound into a `Scan`. This generic class stores an unbound `Controller` method - effectively a function that takes an instance of a specific `Controller` type (`Controller_T`). Instances of this class can be added at `Controller` definition, either manually or with use of the `scan` wrapper, to register the method to be included in the API of the `Controller`. When the `Controller` is instantiated, these instances will be bound to the instance, creating a `Scan` instance. """ def __init__(self, fn: UnboundScanCallback[Controller_T], period: float) -> None: super().__init__(fn) self._period = period @property def period(self): return self._period def _validate(self, fn: UnboundScanCallback[Controller_T]) -> None: super()._validate(fn) if not len(self.parameters) == 1: raise TypeError("Scan method cannot have arguments") def bind(self, controller: Controller_T) -> Scan: return Scan(MethodType(self.fn, controller), self._period)
[docs] def scan( period: float, ) -> Callable[[UnboundScanCallback[Controller_T]], UnboundScanCallback[Controller_T]]: """Decorator to register a `Controller` method as a `Scan` The `Scan` method will be called periodically in the background. """ if period <= 0: raise ValueError("Scan method must have a positive scan period") def wrapper( fn: UnboundScanCallback[Controller_T], ) -> UnboundScanCallback[Controller_T]: setattr(fn, "__unbound_scan__", UnboundScan(fn, period=period)) # noqa: B010 return fn return wrapper