# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.

# %% auto 0
__all__ = ['P', 'VArgs', 'getbound', 'calldeco', 'callwith', 'callattr', 'attrfunc', 'applyfns']

# %% ../nbs/00_core.ipynb 6
from functools import wraps
from inspect import signature, ismethod, Signature

# %% ../nbs/00_core.ipynb 8
from typing import Any, Union, TypeAlias, ParamSpec, Callable, Sequence

# %% ../nbs/00_core.ipynb 10
#| export


# %% ../nbs/00_core.ipynb 12
from atyp import CallQ, DictQ, TupleQ
from nlit import CLS, ARGS, SELF, KWARGS, VKWS, VARG, __DOC__, __NAME__, __DEFAULT__
from chck import issig, isnone
from xpass import xpass

# %% ../nbs/00_core.ipynb 14
#| export


# %% ../nbs/00_core.ipynb 16
P = ParamSpec('P')
VArgs: TypeAlias = Union[Any, tuple]

# %% ../nbs/00_core.ipynb 17
def getbound(fn: Callable, *args: P.args, default: Signature = Signature([]), **kwargs: P.kwargs):
    '''
    Generate the bound arguments of a callable with partial support for variable arguments.

    Parameters
    ----------
    fn : Callable
        The target function whose signature is to be inspected and bound.
    *args : P.args
        Positional arguments to be bound to the function's parameters.
    default : Signature, optional
        A default signature to use if the target function does not have an accessible signature.
    **kwargs : P.kwargs
        Keyword arguments to be bound to the function's parameters, including special variable
        arguments and keyword arguments represented by `VARG` and `VKWS`.

    Returns
    -------
    tuple
        A tuple containing two elements: a tuple of bound positional arguments and a dictionary
        of bound keyword arguments.

    Examples
    --------
    >>> def example_function(a, b, c=3):
    ...     return a + b + c
    >>> args, kwargs = getbound(example_function, 1, 2)
    >>> args
    (1, 2)
    >>> kwargs
    {'c': 3}
    '''
    try:
        sig = signature(fn)
    except ValueError: # Handle built-in functions
        sig = default if issig(default) else Signature([])

    skeys = sig.parameters.keys()
    (bound := sig.bind_partial()).apply_defaults()
    varg, vkws = kwargs.pop(VARG, None), kwargs.pop(VKWS, None)
    k1st = list(skeys)[0] if len(list(skeys)) > 0 else None

    oarg, okws = bound.arguments.get(ARGS, ()), bound.arguments.get(KWARGS, dict())

    for name, value in zip(skeys, args):
        bound.arguments.update({name: value}) 

    if varg:
        for name, value in zip(skeys, varg):
            bound.arguments.update({name: value})

    bound.arguments.update(kwargs)
    if vkws: bound.arguments.update(vkws)

    if ismethod(fn) and k1st == CLS:  bound.arguments.pop(CLS,  None)
    if callable(fn) and k1st == SELF: bound.arguments.pop(SELF, None)
    
    if not isinstance(_a := bound.arguments.get(ARGS), tuple): 
        bound.arguments.update({ARGS: oarg or (_a, )})

    if not isinstance(_k := bound.arguments.get(KWARGS), dict): 
        bound.arguments.update({KWARGS: okws})

    return bound.args, bound.kwargs

# %% ../nbs/00_core.ipynb 18
def calldeco(dec: Callable, fn: CallQ = None, /, *args: P.args, **kwargs: P.kwargs) -> Callable:
    '''
    Decorates a function with the provided decorator and optional arguments.

    Parameters
    ----------
    dec : Callable
        The decorator to be applied.
    fn : CallQ, optional
        The function to be decorated. If not provided, returns the decorator itself.
    *args : P.args
        Positional arguments to be passed to the decorator.
    **kwargs : P.kwargs
        Keyword arguments to be passed to the decorator.

    Returns
    -------
    Callable
        The decorated function.

    Raises
    ------
    TypeError
        If `fn` is not callable.

    Examples
    --------
    >>> def my_decorator(func):
    ...     def wrapper(*args, **kwargs):
    ...         return func(*args, **kwargs)
    ...     return wrapper
    >>> @calldeco(my_decorator)
    ... def my_function(x):
    ...     return x * 2
    >>> my_function(5)
    10
    '''
    if fn is None: return dec
    elif callable(fn): return dec(fn)(*args, **kwargs) # call decorator with arguments if there is a fn to decorate
    else: raise TypeError(f"Expected callable, got {type(fn)}")

# %% ../nbs/00_core.ipynb 19
def callwith(fn: CallQ = None, /, *args: P.args, varg: TupleQ = None, vkws: DictQ = None, **kwargs: P.kwargs):
    '''
    Calls a function with a modified argument list that includes additional variable
    positional and keyword arguments.

    Parameters
    ----------
    fn : CallQ, optional
        The target function to call. If not provided, a decorator is returned.
    *args : P.args
        Positional arguments to be used when calling the function.
    varg : TupleQ, optional
        Additional positional arguments to be merged with `args`.
    vkws : DictQ, optional
        Additional keyword arguments to be merged with `kwargs`.
    **kwargs : P.kwargs
        Initial keyword arguments to be used when calling the function.

    Returns
    -------
    Callable
        A decorated function that, when called, will call the original function with
        the modified argument list.

    Examples
    --------
    >>> def example_function(x, y, z=3):
    ...     return x + y + z
    >>> call_with_fn = callwith(example_function, varg=(2,), vkws={'z': 4})
    >>> call_with_fn(1)
    7
    '''
    # call the fn with the arguments and return the result
    def decorator(fn):
        @wraps(fn)
        def wrapper(*vargs: P.args, **vkwargs: P.kwargs):
            barg, bkws = getbound(fn, *vargs, varg=varg, vkws=vkws, **vkwargs)
            return fn(*barg, **bkws)
        return wrapper
    return calldeco(decorator, fn, *args, **kwargs)    

# %% ../nbs/00_core.ipynb 20
def callattr(obj, attr: str, *args: P.args, **kwargs: P.kwargs): 
    '''
    Calls a method of an object with filtered positional and keyword arguments.

    Parameters
    ----------
    obj : Any
        The object whose method is to be called.
    attr : str
        The name of the method to call.
    *args : P.args
        Positional arguments to pass to the method.
    **kwargs : P.kwargs
        Keyword arguments to pass to the method. A special '__default__' keyword can specify
        a default callable if the attribute does not exist on the object.

    Returns
    -------
    Any
        The result of the method call.

    Examples
    --------
    >>> class MyClass:
    ...     def greet(self, name):
    ...         return f'Hello, {name}!'
    >>> obj = MyClass()
    >>> callattr(obj, 'greet', 'World')
    'Hello, World!'
    '''
    default = kwargs.get(__DEFAULT__, xpass)
    return callwith(getattr(obj, attr, default), *args, **kwargs)

# %% ../nbs/00_core.ipynb 21
def attrfunc(attr: str, *args: P.args, varg: TupleQ = None, vkws: DictQ = None, **kwargs: P.kwargs):
    '''
    Decorates a function to call an attribute of its first argument (assuming it is an object)
    with additional provided arguments.

    Parameters
    ----------
    attr : str
        The name of the attribute to call. If not set, the decorated function's name is used.
    *args : P.args
        Additional positional arguments to pass to the attribute call.
    varg : TupleQ, optional
        Additional positional arguments to be merged with `args` for the attribute call.
    vkws : DictQ, optional
        Additional keyword arguments to be merged with `kwargs` for the attribute call.
    **kwargs : P.kwargs
        Initial keyword arguments to pass to the attribute call.

    Returns
    -------
    Callable
        A decorated function that when invoked, will call the specified attribute on its first
        argument with the provided arguments.

    Examples
    --------
    >>> @attrfunc('greet')
    ... def custom_greet(user):
    ...     pass
    >>> class User:
    ...     def greet(self, greeting):
    ...         return f'User says: {greeting}'
    >>> user = User()
    >>> custom_greet(user, 'Hello')
    'User says: Hello'
    '''
    def decorator(fn: Callable):
        @wraps(fn, assigned=(__DOC__, __NAME__))
        def wrapper(x, *vargs, **vkwargs): 
            nonlocal attr
            if callable(fn) and isnone(attr): attr = fn.__name__
            return callattr(x, attr, *vargs, **vkwargs)
        return wrapper
    return calldeco(decorator, None, *args, **kwargs)

# %% ../nbs/00_core.ipynb 25
def applyfns(x, funcs: Sequence[Callable], *args: P.args, check: CallQ = None, **kwargs: P.kwargs):   
    '''
    Applies a sequence of functions to an initial value `x`, optionally stopping when a check function returns `True`.

    Parameters
    ----------
    x : Any
        The initial value to which the functions will be applied.
    funcs : Sequence[Callable]
        A sequence of functions to apply to `x`.
    *args : P.args
        Positional arguments to pass to each function.
    check : CallQ, optional
        An optional function that takes the same arguments as the functions in `funcs`. If it
        returns `True`, the application of functions is stopped.
    **kwargs : P.kwargs
        Keyword arguments to pass to each function.

    Returns
    -------
    Any
        The result after applying the functions to `x`.

    Examples
    --------
    >>> def increment(x):
    ...     return x + 1
    >>> def double(x):
    ...     return x * 2
    >>> applyfns(2, [increment, double])
    6
    ''' 
    for f in funcs: 
        if callable(check) and callwith(check, x, *args, **kwargs): break
        x = callwith(f, x, *args, **kwargs)
    return x
