Source code for domprob.announcements.decorators

import functools
from collections.abc import Callable
from typing import (
    Any,
    Generic,
    ParamSpec,
    TypeVar,
    cast,
    Concatenate,
)

from domprob.announcements.method import AnnouncementMethod

# Typing helper: Describes the class where the method resides
_MethodCls = TypeVar("_MethodCls", bound=Any)

# Typing helper: Describes the instrument parameters
_Instrument = TypeVar("_Instrument", bound=Any)

# Typing helpers: Describes the method signature
_P = ParamSpec("_P")
_R = TypeVar("_R")
_Meth = Callable[Concatenate[_MethodCls, _Instrument, _P], _R]


[docs] class _Announcement(Generic[_MethodCls, _Instrument, _P, _R]): """Decorator class for associating metadata and validating methods. This class enables the decoration of methods with metadata describing their required instruments. It enforces runtime validation to ensure that the method is called with the correct parameters and that the `instrument` argument satisfies the specified requirements. The `@announcement` decorator can be stacked. .. warning:: It is strongly recommended that instrument classes defined in stacked decorators inherit from the same base class or implement the same typing protocol. Args: instrument (type[_Instrument]): The instrument class required by the decorated method. required (bool): Whether the instrument is required. Defaults to `False`. Examples: Simple implementation: >>> class PrintInstrument: ... ... @staticmethod ... def stdout(msg: str) -> None: ... print(msg) ... ... def __repr__(self) -> str: ... return f"{self.__class__.__name__}()" ... >>> # Define a class with a decorated method >>> from domprob import announcement >>> >>> class Foo: ... @announcement(PrintInstrument) ... def bar(self, instrument: PrintInstrument) -> None: ... instrument.stdout(f"Executing with {instrument!r}") ... >>> foo = Foo() >>> instru = PrintInstrument() >>> >>> foo.bar(instru) Executing with PrintInstrument() Supporting the same announcement implementation with multiple instruments: >>> import logging >>> from abc import ABC, abstractmethod >>> >>> # Define instruments >>> class AbstractStdOutInstrument(ABC): ... @abstractmethod ... def stdout(self, cls_name: str) -> None: ... raise NotImplementedError ... ... def __repr__(self) -> str: ... return f"{self.__class__.__name__}()" ... >>> class PrintInstrument(AbstractStdOutInstrument): ... def stdout(self, cls_name: str) -> None: ... print(f"Observing '{cls_name}' with '{self!r}'\") ... >>> class LogInstrument(AbstractStdOutInstrument): ... ... def __init__(self): ... self.logger = logging.getLogger() ... self.logger.setLevel(logging.INFO) ... ... def stdout(self, cls_name: str) -> None: ... logger = logging.getLogger() ... logger.setLevel(logging.INFO) ... logger.info(f"Observing '{cls_name}' with '{self!r}'\") ... >>> # Define a class with a decorated method >>> from domprob import announcement >>> >>> class Foo: ... @announcement(PrintInstrument) ... @announcement(LogInstrument) ... def bar(self, instrument: AbstractStdOutInstrument) -> None: ... instrument.stdout(self.__class__.__name__) ... >>> foo = Foo() >>> instru = PrintInstrument() >>> >>> foo.bar(instru) Observing 'Foo' with 'PrintInstrument()' """ def __init__( self, instrument: type[_Instrument], required: bool = False ) -> None: self.instrument = instrument self.required = required
[docs] def __call__(self, method: _Meth) -> Callable[_P, _R]: """Wraps a method to associate metadata and enforce runtime validation. This method is invoked when the `@announcement` decorator is used on a method. It attaches metadata, including the instrument class and requirement status, to the method and enforces validation when the method is called at runtime. Args: method (Callable[P, R]): The method to decorate. Returns: Callable[P, R]: A wrapped version of the input method with metadata and validation applied. Examples: >>> class SomeInstrument: ... pass ... >>> # Define a class with a decorated method >>> from domprob import announcement >>> >>> class Foo: ... @announcement(SomeInstrument) ... def bar(self, instrument: SomeInstrument) -> None: ... print(f"Executing with {instrument!r}") ... >>> foo = Foo() >>> instru = SomeInstrument() >>> >>> foo.bar(instru) Executing with <...SomeInstrument object at 0x...> """ meth = AnnouncementMethod(method) meth.supp_instrums.record(self.instrument, self.required) @functools.wraps(method) def wrapper( cls_instance: _MethodCls, instrument: _Instrument, /, *args: _P.args, **kwargs: _P.kwargs, ) -> _R: bound_meth = meth.bind(cls_instance, instrument, *args, **kwargs) bound_meth.validate() return bound_meth.execute() return cast(Callable[_P, _R], wrapper)
[docs] def __repr__(self) -> str: # noinspection PyShadowingNames """Returns a string representation of the `Announcement` instance. This method provides a concise, informative string representation of the `Announcement` instance, including its instrument class and requirement status. Returns: str: A string representation of the `Announcement` instance. Examples: >>> class SomeInstrument: ... pass ... >>> announcement = _Announcement(SomeInstrument) >>> repr(announcement) "_Announcement(instrument=<class '...SomeInstrument'>)" """ return f"{self.__class__.__name__}(instrument={self.instrument!r})"
# pylint: disable=invalid-name announcement = _Announcement # Alias to be pythonic