from __future__ import annotations
from collections.abc import Callable
from typing import (
Any,
Concatenate,
Generic,
ParamSpec,
TypeVar,
)
from domprob.sensors.base_meth import BaseSensorMethod
from domprob.sensors.bound_meth import BoundSensorMethod
from domprob.sensors.instrums import Instruments
from domprob.sensors.meth_binder import SensorMethodBinder
# Typing helpers: Describes the wrapped method signature for wrapper
_PMeth = ParamSpec("_PMeth")
_RMeth = TypeVar("_RMeth")
[docs]
class SensorMethod(BaseSensorMethod, Generic[_PMeth, _RMeth]):
"""Represents a decorated method with associated metadata.
This class acts as a wrapper and provides an interface to interact
with the supported instruments of a method decorated with
`@sensors`. It also facilitates partially binding runtime
arguments to the method before method execution.
Args:
meth (`Callable[P, R]`): The decorated method to be
managed.
Examples:
>>> class SomeInstrument:
... pass
...
>>> # Define a class with a decorated method
>>> from domprob import sensor
>>>
>>> class Foo:
... @sensor(SomeInstrument)
... def bar(self, instrument: SomeInstrument) -> None:
... pass
...
>>> # Create an SensorMethod instance
>>> bar_method = SensorMethod(Foo.bar)
>>>
>>> bar_method
SensorMethod(meth=<function Foo.bar at 0x...>)
"""
__slots__: list[str] = ["_meth", "_supp_instrums", "_binder"]
_binder: SensorMethodBinder[_PMeth, _RMeth]
def __init__(
self,
meth: Callable[_PMeth, _RMeth],
*,
static: bool = False,
supp_instrums: Instruments[Any] | None = None,
) -> None:
super().__init__(meth, static=static, supp_instrums=supp_instrums)
self._binder = SensorMethodBinder(self)
[docs]
@classmethod
def from_callable(
cls, meth: Callable[_PMeth, _RMeth]
) -> SensorMethod[_PMeth, _RMeth] | None:
"""Creates an `SensorMethod` instance from a callable if
it supports instruments.
This class method checks if the provided callable (`meth`) has
associated metadata for supported instruments. If it does, an
`SensorMethod` instance is created and returned.
Otherwise, `None` is returned.
Args:
meth (Callable[_PMeth, _RMeth]): The method or function to
be wrapped as an `SensorMethod`.
Returns:
SensorMethod[_PMeth, _RMeth] | None:
- An instance of `SensorMethod` if the callable
has associated metadata.
- `None` if the callable does not support instruments.
Example:
>>> from domprob import sensor
>>>
>>> class SomeInstrument:
... pass
...
>>> class Foo:
... @sensor(SomeInstrument)
... def bar(self, instrument: SomeInstrument) -> None:
... print(f"Instrument: {instrument}")
...
>>> # Create an SensorMethod instance from a method
>>> sensor_meth = SensorMethod.from_callable(Foo.bar)
>>> assert isinstance(sensor_meth, SensorMethod)
>>> print(sensor_meth)
SensorMethod(meth=<function Foo.bar at 0x...>)
>>> # Attempt to create an SensorMethod from a method without metadata
>>> def no_sensor_method():
... pass
...
>>> assert SensorMethod.from_callable(no_sensor_method) is None
"""
instrums = Instruments.from_method(meth)
return cls(meth, supp_instrums=instrums) if instrums else None
[docs]
def bind(
self, *args: _PMeth.args, **kwargs: _PMeth.kwargs
) -> BoundSensorMethod[Concatenate[Any, _PMeth], _RMeth]:
# noinspection PyShadowingNames
# pylint: disable=line-too-long
"""Binds passed parameters to the method, returning a
partially bound version.
This method partially binds the provided runtime arguments. It
returns a `BoundSensorMethod` object that represents the
partially bound method, which can later be executed with
additional arguments if needed.
Args:
cls_instance (`Any`): The class instance to bind. This is
the `self` arg defined in instance methods.
*args (P.args): Additional positional arguments to bind to
the method.
**kwargs (P.kwargs): Additional keyword arguments to bind
to the method.
Returns:
BoundSensorMethod: A new wrapper representing a
partially bound method.
Examples:
>>> class SomeInstrument:
... pass
...
>>> # Define a class with a decorated method
>>> from domprob import sensor
>>>
>>> class Foo:
... @sensor(SomeInstrument)
... def bar(self, instrum: SomeInstrument) -> None:
... pass
...
>>> # Create an SensorMethod instance
>>> bar_method = SensorMethod(Foo.bar)
>>>
>>> # Create an instance of the class and instrument
>>> instrument_instance = SomeInstrument()
>>> foo = Foo()
>>>
>>> # Binds method with instrument instance
>>> args = (foo, instrument_instance)
>>> bound_method = bar_method.bind(*args)
>>> bound_method
BoundSensorMethod(sensor_meth=SensorMethod(meth=<function Foo.bar at 0x...>), bound_params=<BoundArguments (self=<domprob.sensors.meth.Foo object at 0x...>, instrum=<domprob.sensors.meth.SomeInstrument object at 0x...>)>)
"""
return self._binder.bind(*args, **kwargs)