Source code for laser_measles.components.utils
"""
Component utilities for the laser-measles package.
This module provides utilities for creating and managing components in the laser-measles package.
The main feature is a decorator that makes it easier to create components with parameters.
"""
from collections.abc import Callable
from functools import wraps
from typing import Any
from typing import TypeVar
from pydantic import BaseModel
from laser_measles.base import BaseComponent
T = TypeVar("T", bound=BaseComponent)
B = TypeVar("B", bound=BaseModel)
[docs]
def component(cls: type[T] | None = None, **default_params):  # noqa: UP047
    """
    Decorator for creating components with default parameters.
    This decorator makes it easier to create components with parameters by:
    1. Allowing default parameters to be specified at class definition time
    2. Creating a factory function that can be used to create component instances
    3. Preserving type hints and docstrings
    Parameters
    ----------
    cls : Type[BaseComponent], optional
        The component class to decorate. If None, returns a decorator function.
    **default_params
        Default parameters to use when creating the component instance.
    Returns
    -------
    Union[Type[BaseComponent], Callable]
        If cls is provided, returns a factory function for creating component instances.
        If cls is None, returns a decorator function.
    Examples
    --------
    Basic usage:
    >>> @component
    ... class MyComponent(BaseComponent):
    ...     def __init__(self, model, verbose=False, param1=1, param2=2):
    ...         super().__init__(model, verbose)
    ...         self.param1 = param1
    ...         self.param2 = param2
    With default parameters:
    >>> @component(param1=10, param2=20)
    ... class MyComponent(BaseComponent):
    ...     def __init__(self, model, verbose=False, param1=1, param2=2):
    ...         super().__init__(model, verbose)
    ...         self.param1 = param1
    ...         self.param2 = param2
    Using the factory:
    >>> # Create with default parameters
    >>> MyComponent.create(model)
    >>> # Create with custom parameters
    >>> MyComponent.create(model, param1=100, param2=200)
    """
    def decorator(component_cls: type[T]) -> type[T]:
        # Store the default parameters
        component_cls._default_params = default_params  # type: ignore
        # Create a factory function for creating instances
        @wraps(component_cls)
        def create(model: Any, **kwargs) -> T:
            # Merge default parameters with provided parameters
            params = {**default_params, **kwargs}
            return component_cls(model, **params)
        # Add the factory function to the class
        component_cls.create = staticmethod(create)  # type: ignore
        return component_cls
    # If cls is provided, apply the decorator immediately
    if cls is not None:
        return decorator(cls)
    # Otherwise, return the decorator function
    return decorator 
[docs]
def create_component(component_class: type[T], params: type[B] | None = None) -> Callable[[Any, Any], T]:  # noqa: UP047
    """
    Helper function to create a component instance with parameters.
    This function creates a callable object that will instantiate the component
    with the given parameters when called by the model.
    Parameters
    ----------
    component_class : Type[BaseComponent]
        The component class to instantiate
    **kwargs
        Parameters to pass to the component constructor
    Returns
    -------
    Callable[[Any, Any], BaseComponent]
        A function that creates the component instance when called by the model
    Examples
    --------
    >>> model.components = [
    ...     create_component(MyComponent, params=MyComponentParams),
    ...     AnotherComponent,
    ... ]
    """
    class ComponentFactory:
        def __init__(self, component_class: type[T], params: BaseModel | None = None):
            self.component_class = component_class
            if params is not None:
                self.params = params
            else:
                self.params = None
        def __call__(self, model: Any, verbose: bool = False) -> T:
            return self.component_class(model, params=self.params, verbose=verbose)
        def __str__(self) -> str:
            return f"<{self.component_class.__name__} factory>"
        def __repr__(self) -> str:
            return f"<{self.component_class.__name__} factory>"
    return ComponentFactory(component_class, params)