fabiommendes/sidekick

View on GitHub
sidekick-functions/sidekick/functions/fn_mixins.py

Summary

Maintainability
A
1 hr
Test Coverage
from functools import cached_property

from ..typing import TYPE_CHECKING, Any, Callable, Catchable

if TYPE_CHECKING:
    from .fn import fn


def as_method(method, name):
    method.__name__ = name
    method.__qualname__ = f"fn.{name}"
    method.__doc__ = "Auto generated, see :func:`sidekick.functions.{name}`"
    return method


def curry_n(n, name, options=()):
    """
    Curry if only one argument is given and execute if any additional arguments
    are passed.
    """

    def method(self: "FnMixin", *args, **kwargs):
        api_func = getattr(self._mod, name)
        api_kwargs = {k: kwargs.pop(k) for k in kwargs if k in options}

        if len(args) == n and not kwargs:
            return api_func(*args, self, **api_kwargs)

        transformed_fn = api_func(*args[:n], self, **api_kwargs)
        return transformed_fn(*args[n:], **kwargs)

    return as_method(method, name)


class FnMixin:
    """
    Basic mixin class that exposes a _mod lazy attribute to access the
    sidekick.functions module and declare the _func attribute that is
    manipulated by methods.
    """

    _func: Callable
    __call__: Callable
    __slots__ = ()

    if TYPE_CHECKING:
        from .. import functions as _mod

        _mod = _mod
    else:

        @cached_property
        def _mod(self):
            from .. import functions

            return functions

    #
    # Expose functions in sidekick.functions.lib_arguments as methods.
    #
    def flip(self, x, y, /, *args, **kwargs):
        """
        Executes flipping the first two arguments.

        Access as attribute to obtain a flipped version of function.
        """
        return self._func(y, x, *args, **kwargs)

    def reverse_args(self, /, *args, **kwargs):
        """
        Executes reversing the order of positional arguments.

        Access as attribute to obtain a reversed version of function.
        """
        return self._func(*args[::-1], **kwargs)

    select_args = curry_n(1, "select_args")
    skip_args = curry_n(1, "skip_args")
    keep_args = curry_n(1, "keep_args")

    def variadic_args(self, /, *args, **kwargs):
        """
        Pass variadic arguments as single tuple to function.
        """
        return self._func(args, **kwargs)

    def splice_args(self, xs, /, *args, **kwargs):
        """
        Splice first argument.
        """
        return self._func(*xs, *args, **kwargs)

    def set_null(self, /, *defaults: Any, **kwargs: Any) -> "fn":
        """
        Return a new function that replace all null arguments in the given positions
        by the provided default value.
        """
        return self._mod.set_null(self._func, *defaults, **kwargs)

    #
    # Expose functions in sidekick.functions.lib_combinators as methods.
    #
    def do(self, /, *args, **kwargs):
        """
        Execute function, but return the first argument.

        Function result is ignored, hence do is executed only for the function
        side-effects.
        """
        if not args:
            raise TypeError("requires at least a single argument.")
        self(*args, **kwargs)
        return args[0]

    #
    # Expose functions in sidekick.functions.lib_composition as methods.
    #
    def compose(self, *funcs) -> "fn":
        """
        Compose with other functions.

        Argument flow from right to left. Function is thus the last to execute.
        """
        return self._mod.compose(self, *funcs)

    def pipeline(self, *funcs) -> "fn":
        """
        Compose with other functions.

        Argument flow from left to right, starting in self.
        """
        return self._mod.pipeline(self, *funcs)

    def juxt(self, *funcs, **kwargs) -> "fn":
        """
        Return function that juxtaposes fn with all functions in the arguments.
        """
        return self._mod.juxt(self, *funcs, **kwargs)

    #
    # Expose functions in sidekick.functions.lib_runtime as methods.
    #
    def once(self) -> "fn":
        """
        Version of function that perform a single invocation.

        Repeated calls to the function return the value of the first invocation.
        """
        return self._mod.once(self._func)

    def thunk(self, /, *args, **kwargs) -> Callable[[], Any]:
        """
        Return as a thunk.
        """
        return self._mod.thunk(self, *args, **kwargs)

    call_after = curry_n(1, "call_after", {"default"})
    call_at_most = curry_n(1, "call_at_most")

    def throttle(self, dt: float, **kwargs) -> "fn":
        """
        Limit the rate of execution of func to once at each ``dt`` seconds.

        Return a new function.
        """
        return self._mod.throttle(dt, self, **kwargs)

    def background(self, /, *args, **kwargs) -> Any:
        """
        Execute function in the background.

        Current implementation uses threads, but in the future it may use hooks
        to other runtimes such as asyncio, curio, etc.
        """
        return self._mod.background(self, *args, **kwargs)

    def catch(self, error, /, *args, **kwargs):
        """
        Handle exception in function.

        If the exception occurs, return None or the value mapped from the error
        mapping.
        """
        return self._mod.catch(error, self, *args, **kwargs)

    def catching(self, error) -> "fn":
        """
        Handle exception in function.

        If the exception occurs, it executes the given handler.

        Return a new function with the new error handling behavior.
        """
        return self._mod.catching(error, self)

    def retry(
        self, n: int, /, *args, error: Catchable = Exception, sleep=None, **kwargs
    ) -> "fn":
        """
        Try to call function n types before raising an error.

        This is useful for functions that may fail due to interaction with
        external resources (e.g., fetch data from the network).

        Args:
            n:
                Maximum number of times to execute function
            error:
                Exception or tuple with suppressed exceptions.
            sleep:
                Interval between attempts. This is a blocking function, hence
                use with care.

        Other positional and keyword arguments are forwarded to function.
        """

        func = self._mod.retry(n, self, error=error, sleep=sleep)
        return func(*args, **kwargs)