Source code for fastcs.methods.command
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__)
UnboundCommandCallback = Callable[[Controller_T], Coroutine[None, None, None]]
"""A Command callback that is unbound and must be called with a `Controller` instance"""
CommandCallback = Callable[[], Coroutine[None, None, None]]
"""A Command callback that is bound and can be called without `self`"""
[docs]
class Command(Method["BaseController"]):
"""A `Controller` `Method` that performs a single action when called.
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: CommandCallback, *, group: str | None = None):
super().__init__(fn, group=group)
def _validate(self, fn: CommandCallback) -> None:
super()._validate(fn)
if not len(self.parameters) == 0:
raise TypeError(f"Command method cannot have arguments: {fn}")
async def __call__(self):
return await self.fn()
@property
def fn(self) -> CommandCallback:
async def command():
try:
return await self._fn()
except Exception:
logger.exception("Command failed", fn=self._fn)
raise
return command
[docs]
class UnboundCommand(Method[Controller_T]):
"""A wrapper of an unbound `Controller` method to be bound into a `Command`.
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 `command` 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 `Command` instance.
"""
def __init__(
self, fn: UnboundCommandCallback[Controller_T], *, group: str | None = None
) -> None:
super().__init__(fn, group=group)
def _validate(self, fn: UnboundCommandCallback[Controller_T]) -> None:
super()._validate(fn)
if not len(self.parameters) == 1:
raise TypeError("Command method cannot have arguments")
def bind(self, controller: Controller_T) -> Command:
return Command(MethodType(self.fn, controller), group=self.group)
[docs]
def command(
*, group: str | None = None
) -> Callable[
[UnboundCommandCallback[Controller_T]], UnboundCommandCallback[Controller_T]
]:
"""Decorator to register a `Controller` method as a `Command`
The `Command` will be passed to the transport layer to expose in the API
:param: group: Group to display this command under in the transport layer
"""
def wrapper(
fn: UnboundCommandCallback[Controller_T],
) -> UnboundCommandCallback[Controller_T]:
setattr(fn, "__unbound_command__", UnboundCommand(fn, group=group)) # noqa: B010
return fn
return wrapper