# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_futil.ipynb.

# %% auto 0
__all__ = ['hasvargs', 'haskwargs', 'islambda', 'lambdastr_inspect', 'lambdastr_re', 'lambdastr', 'mergevargs', 'getdefs']

# %% ../nbs/01_futil.ipynb 3
import re, inspect
from typing import Tuple, Callable

# %% ../nbs/01_futil.ipynb 5
def hasvargs(f: Callable) -> bool:
    '''
    Checks if a given callable has variable positional arguments.
    
    Parameters
    ----------
    f : Callable
        The function to check for variable positional arguments.
        
    Returns
    -------
    bool
        True if the function has variable positional arguments, otherwise False.
    '''
    if not inspect.isfunction(f): return False
    return any(p.kind == p.VAR_POSITIONAL for p in inspect.signature(f).parameters.values())

def haskwargs(f: Callable) -> bool:
    '''
    Checks if a given callable has variable keyword arguments.
    
    Parameters
    ----------
    f : Callable
        The function to check for variable keyword arguments.
        
    Returns
    -------
    bool
        True if the function has variable keyword arguments, otherwise False.
    '''
    if not inspect.isfunction(f): return False
    return any(p.kind == p.VAR_KEYWORD for p in inspect.signature(f).parameters.values())

def islambda(f: Callable) -> bool:
    '''
    Checks if a given callable is a lambda function.
    
    Parameters
    ----------
    f : Callable
        The function to check.
        
    Returns
    -------
    bool
        True if the function is a lambda, otherwise False.
    '''
    return inspect.isfunction(f) and f.__name__ == '<lambda>'

def lambdastr_inspect(f: Callable) -> str:   
    # Use inspect.getsource() to get the source code of the lambda function
    code = inspect.getsource(f)
    # Extract the lambda expression part from the source code
    expr = code.strip().split(':')[1].strip().removeprefix('(').removesuffix(')')
    return expr

def lambdastr_re(f: Callable) -> str: 
    # Use inspect.getsource() to get the source code of the lambda function
    code = inspect.getsource(f).strip()
    
    # Use regex to find the lambda expression in the source code
    match = re.search(r'lambda[^:]+:[^,)]+', code)
    # match = re.search(r'lambda[^:]+:([^,)]+)', code)
    if match: 
        full_lambda = match.group().strip()
        return full_lambda.strip().split(':')[1].strip()
        return match.group().strip()
    

def lambdastr(f: Callable) -> str:
    '''
    Retrieves the string representation of a lambda function.
    
    Parameters
    ----------
    f : Callable
        The lambda function.
        
    Returns
    -------
    str
        The string representation of the lambda function.
    '''
    return lambdastr_re(f) or lambdastr_inspect(f)

# %% ../nbs/01_futil.ipynb 6
def mergevargs(vargs: tuple, *args):
    '''Merges two tuples, prioritizing non-None values from the second tuple.
    
    This function takes a default tuple `vargs` and merges it with another tuple `args`.
    If an index `i` in `args` has a non-None value, it replaces the value at index `i` in `vargs`.
    Additional elements in `args` beyond the length of `vargs` are appended to the end.

    Parameters
    ----------
    vargs : tuple
        The default tuple of values.

    args : tuple
        The tuple of new values to merge.

    Returns
    -------
    tuple
        A new tuple containing the merged values.

    Examples
    --------
    >>> vargs((None, 2, 3), 1, None, 4)
    (1, 2, 4)

    >>> vargs((None, None), 1, 2, 3)
    (1, 2, 3)

    >>> vargs((1, 2), None)
    (1, 2)

    >>> vargs((1, 2))
    (1, 2)
    '''
    res = [new if i < len(args) and (new := args[i]) is not None else old for i, old in enumerate(vargs)]
    res.extend(args[len(vargs):])
    return tuple(res)

# %% ../nbs/01_futil.ipynb 7
def getdefs(func: Callable, varg: tuple, kwds: dict, *args, **kwargs) -> Tuple[tuple, dict]:
    '''Get the arguments by applying defaults.
    
    Parameters
    ----------
    func : Callable
        The function whose signature is to be considered.
    varg : tuple
        Default positional arguments.
    kwds : dict
        Default keyword arguments.
    args : tuple
        New positional arguments.
    kwargs : dict
        New keyword arguments.
    
    Returns
    -------
    dict
        Bound arguments.
    '''
    fn_sig = inspect.signature(func)
    anames = list(fn_sig.parameters.keys()) # argument names

    newvarg = list(mergevargs(varg, *args))
    newkwds = {**kwds, **kwargs}
    
    # Remove conflicting keys from newkwds
    for i, key in enumerate(anames[:len(newvarg)]):
        if (arg := newkwds.pop(key, None)) is not None:        
            newvarg[i] = arg
    
    bound = fn_sig.bind(*newvarg, **newkwds)
    bound.apply_defaults()
    return bound.args, bound.kwargs
