fabiommendes/sidekick

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

Summary

Maintainability
A
55 mins
Test Coverage
from functools import partial as _partial, wraps
from operator import methodcaller

from .core_functions import arity, to_callable, quick_fn
from .fn import fn, Curried
from ..typing import Func, Callable, overload, TYPE_CHECKING

if TYPE_CHECKING:
    from .. import api as sk  # noqa: F401


def partial(func, /, *args, **kwargs) -> fn:
    """
    Return a new function that partially apply the given arguments and
    keywords.

    Additional positional and keyword arguments after partially applied to
    function

    Args:
        func:
            Function or func-like object.

    Examples:
        >>> from operator import add
        >>> incr =  sk.partial(add, 1)
        >>> incr(41)
        42

    See Also:
        :func:`rpartial`
    """
    return quick_fn(_partial(to_callable(func), *args, **kwargs).__call__)


def rpartial(func: Func, /, *args, **kwargs) -> fn:
    """
    Partially apply arguments from the right.

    Examples:
        >>> from operator import truediv as div
        >>> half = sk.rpartial(div, 2)
        >>> half(42)
        21.0

    See Also:
        :func:`partial`
    """
    func = to_callable(func)
    return quick_fn(lambda *args_, **kwargs_: func(*args_, *args, **kwargs, **kwargs_))


@overload
def curry(n: int, func: Callable) -> Curried:
    ...


@overload
def curry(n: int) -> Callable[[Callable], Curried]:  # noqa: F811
    ...


def curry(n, func=None):  # noqa: F811
    """
    Return the curried version of a function of n arguments.

    Curried functions return partial applications of the function if called with
    missing arguments:

    >>> add = sk.curry(2, lambda x, y: x + y)

    We can call a function two ways:

    >>> add(1, 2) == add(1)(2)
    True

    This is useful for building simple functions from partial application

    >>> succ = add(1)
    >>> succ(2)
    3

    :func:`curry` is itself a curried function, hence it can be called as

    >>> add = sk.curry(2)(lambda x, y: x + y)

    or equivalently as a decorator

    >>> @sk.curry(2)
    ... def add(x, y):
    ...     return x + y


    Currying usually requires functions of fixed number of arguments (the
    number of arguments is called the *arity* of a function). We can control
    how many arguments participate in the auto-currying by passing the arity
    number as the first argument to the :func:`curry` function.

    Variadic functions are accepted, and arity is understood as the minimum
    number of arguments necessary to invoke the function. The caller can,
    however, specify additional arguments.

    But it accepts more than 2 arguments, if needed. (Notice that only the
    first two arguments auto-curry.)

    >>> add = sk.curry(2, lambda *args: sum(args))
    >>> add(1, 2, 3, 4)
    10

    Sometimes we don't want to specify the arity of a function or don't want
    to think too much about it. :func:`curry` accepts ``'auto'`` as an arity
    specifier that makes it try to infer the arity automatically. Under the
    hood, it just calls :func:`arity` to obtain the correct value.

    Sidekick curries most functions where it makes sense. Variadic functions
    cannot be curried if the extra arguments can be passed by position. This
    decorator inspect the decorated function to determine if it can be curried
    or not.
    """

    # Decorator forms
    if callable(n):
        func: Callable = n
        return curry(arity(func), func)
    if func is None:
        return quick_fn(lambda f: curry(n, f))
    else:
        n = arity(func) if n in (..., None, "auto") else n
        if n == 0:
            raise TypeError("cannot curry function that receives no arguments")
        return fn.curry(n, func)


class _fn_method(fn):
    __slots__ = ()
    __doc__ = None

    def __init__(self, func):
        super().__init__(func)
        wraps(func)(self)

    def __getattr__(self, item):
        if item.startswith('_'):
            raise AttributeError(item)
        return lambda *args, **kwargs: methodcaller(item, *args, **kwargs)


@_fn_method
def method(*args, **kwargs):
    """
    Return a function that calls a method of its argument with the given values.

    A method caller object. It can be used as a function

    >>> pop_first = sk.method("pop", 0)
    >>> pop_first([1, 2, 3])
    1

    or as a function factory.

    >>> pop_first = sk.method.pop(0)
    >>> pop_first([1, 2, 3])
    1

    The second usage is syntactically cleaner and prevents the usage of
    invalid Python names. All method calls performed in the ``sk.method`` object
    returns the corresponding methodcaller function.
    """
    return methodcaller(*args, **kwargs)