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

# %% auto 0
__all__ = ['isp', 'iscls', 'isp_non_self', 'isp_non_empty', 'isp_w_default', 'is_attrkeys', 'isint', 'null_cls_init_sig',
           'getmro', 'getinit', 'getannots', 'getannots_kws', 'make_attrvals', 'attrkeys_as_dict', 'getattrs',
           'getinit_sig', 'getinit_kws', 'getinit_kws_defaults', 'funkws', 'kwskws', 'unpack_optout',
           'verb_get_kws_at_lvl', 'get_kws_at_lvl', 'getattrs_at_lvl', 'get_kws_cur', 'get_kws_all', 'get_kws_inh',
           'getattrs_cur', 'getattrs_all', 'getattrs_inh']

# %% ../nbs/01_utils.ipynb 4
import inspect, string
from inspect import (
    Parameter, Signature, Arguments, ArgInfo, BoundArguments,
    isclass, isfunction, signature, iskeyword, isbuiltin, getsource, getmro, getmembers, getmembers_static,
    getcallargs, getargvalues, dis, get_annotations, getclasstree, 
)

from dataclasses import dataclass, KW_ONLY

# %% ../nbs/01_utils.ipynb 5
from typing import (
    Type, TypeAlias, TypeGuard, Optional, Union, 
    Mapping, Callable, Any, Tuple, List, Dict
)

# %% ../nbs/01_utils.ipynb 6
from kwix.types import (
    Sig, Param, ObjQ, StrQ, ClassLevel,
    AttrKeysLike, AttrKeysLikeQ, AttrDefaults
)

# %% ../nbs/01_utils.ipynb 9
def isp(p: Param) -> TypeGuard[Param]:
    return isinstance(p, Parameter)

def iscls(obj: Type) -> TypeGuard[Type]:
    return inspect.isclass(obj)

def isp_non_self(p: Param) -> bool:
    return isp(p) and p.name != 'self'

def isp_non_empty(p: Param) -> bool:
    return isp(p) and p.default != Parameter.empty

def isp_w_default(p: Param) -> bool:
    return isp_non_self(p) and isp_non_empty(p)

def is_attrkeys(attrkeys: AttrKeysLikeQ = None) -> TypeGuard[AttrKeysLike]:
    return isinstance(attrkeys, (list, dict))

def isint(x) -> TypeGuard[int]:
    return isinstance(x, int)

# %% ../nbs/01_utils.ipynb 12
def null_cls_init_sig(self, *args, **kwargs):
    pass

def getmro(obj) -> tuple:
    return getattr(obj, '__mro__', ())

def getinit(obj) -> Callable:    
    return getattr(obj, '__init__', null_cls_init_sig)

def getannots(obj) -> dict:
    '''Get annotations of a callable'''
    return getattr(obj, '__annotations__', {})

def getannots_kws(obj) -> list:        
    '''Get class annotationed keyword only arguments names'''
    annots = list(getannots(obj).keys())
    i_for_ = '_' in annots and annots.index('_')
    return [] if i_for_ is False else annots[i_for_ + 1:]


# %% ../nbs/01_utils.ipynb 14
def make_attrvals(attrkeys: AttrKeysLikeQ = None, attrvals: AttrDefaults = None) -> AttrDefaults:
    attrkeys = (attrkeys or [])
    if not is_attrkeys(attrkeys): return []

    if isinstance(attrkeys, dict): return list(attrkeys.values())

    defaults = [None] * len(attrkeys)
    if isinstance(attrvals, list):
        defaults[:len(attrvals)] = attrvals
    return defaults

def attrkeys_as_dict(attrkeys: AttrKeysLikeQ = None, attrvals: AttrDefaults = None) -> Dict[str, Any]:
    attrkeys = (attrkeys or [])
    if not isinstance(attrkeys, (list, dict)):  return {}

    if isinstance(attrkeys, dict): return attrkeys

    attrvals = make_attrvals(attrkeys, attrvals)
    return {k: attrvals[i] for i, k in enumerate(attrkeys)}

# %% ../nbs/01_utils.ipynb 15
def getattrs(obj, attrs: AttrKeysLikeQ = None,  attrvals: AttrDefaults = None) -> dict:
    '''Get attributes values of an object'''
    adict = attrkeys_as_dict(attrs, attrvals)
    return dict((k, getattr(obj, k, v)) for k, v in adict.items())

# %% ../nbs/01_utils.ipynb 17
def getinit_sig(obj) -> Sig:
    '''Get init signature of a callable'''     
    return inspect.signature(getinit(obj))    

def getinit_kws(obj) -> list:        
    '''Get class keyword only argument names'''
    return inspect.getfullargspec(getinit(obj)).kwonlyargs

def getinit_kws_defaults(obj) -> dict:
    sig = getinit_sig(obj)
    kws = dict((k, p.default) for k, p in sig.parameters.items() if isp_w_default(p))    
    return kws

# %% ../nbs/01_utils.ipynb 19
def funkws(func: Optional[Callable] = None, **kwargs) -> dict:
    '''Filter keyword arguments to function signature'''
    if not func: return kwargs    
    sig = inspect.signature(func)
    return {k: v for k, v in kwargs.items() if k in sig.parameters}

def kwskws(kws: Optional[dict] = None, **kwargs) -> dict:    
    '''merge kwargs into kws if kws is not None otherwise return kwargs'''
    if not kws: return kwargs
    return {**kws, **{k: v for k, v in kwargs.items() if k in kws}}


# %% ../nbs/01_utils.ipynb 21
def unpack_optout(output: tuple, expected: int = 1) -> tuple:
    """Unpacks the output of a function with any number of optional return values."""
    res = output if isinstance(output, tuple) else (output,)
    try:
        res, *opt = res
        if len(opt) < expected:
            opt += [None] * (expected - len(opt))        
        return res, *tuple(opt)
    except TypeError:
        return res, *tuple([None] * max((expected), 1))
    return output

# %% ../nbs/01_utils.ipynb 24
def verb_get_kws_at_lvl(lvl: ClassLevel, cls: ClassLevel, verbose: bool = False):
    tabs = '\t' * lvl
    tmpl = string.Template('$tabs lvl = $lvl\tcls = $cls')
    sout = tmpl.substitute(tabs=tabs, lvl=lvl, cls=cls.__name__)
    if verbose: print(sout)
    if verbose and cls is object:
        print(f'{tabs}\t\tis object --> continue')
        
def get_kws_at_lvl(obj, lvl: ClassLevel, agg: bool = True, return_cls: bool = False, verbose: bool = False) -> Union[dict, Tuple[dict, Type]]:
    result = []
    for curlvl, curcls in enumerate(inspect.getmro(obj)):        
        verb_get_kws_at_lvl(curlvl, curcls, verbose=verbose)
        if curcls is object: continue

        clslvl = curlvl if isint(lvl) else curcls
        curkws = getannots_kws(curcls)
        
        # If we've gone too far i.e. curlvl >= lvl or curcls is no longer a subclass
        if clslvl == lvl:
            result.extend(kw for kw in curkws if kw not in result)
            break
        
        # Otherwise, add the keywords to the result        
        result.extend(kw for kw in curkws if kw not in result)
    
    result = sorted(result if agg else curkws)
    if return_cls: return result, curcls
    return result


# %% ../nbs/01_utils.ipynb 26
def getattrs_at_lvl(obj, lvl: ClassLevel, agg: bool = True, use_lvl_vals: bool = False, verbose: bool = False) -> dict:
    kws, lvlcls = get_kws_at_lvl(obj, lvl, agg, return_cls=True, verbose=verbose)
    lvlcls = (lvlcls if isint(lvl) else lvl)
    
    lvlkws = getattrs(lvlcls, kws)
    curkws = getattrs(obj, kws)
    params = lvlkws.copy()
    if not agg:
        return params

    if use_lvl_vals: 
        params.update({k: v for k, v in curkws.items() if lvlkws.get(k, None) is None})    
    else:
        params.update(curkws)
    return params

# %% ../nbs/01_utils.ipynb 28
def get_kws_cur(obj, verbose: bool = False) -> list:
    kws = get_kws_at_lvl(obj, 0, agg=True, return_cls=False, verbose=verbose)
    return kws

def get_kws_all(obj, verbose: bool = False) -> list:
    kws = get_kws_at_lvl(obj, float('inf'), agg=True, return_cls=False, verbose=verbose)
    return kws

def get_kws_inh(obj, verbose: bool = False) -> list:
    curkws = get_kws_cur(obj, verbose=verbose)
    allkws = get_kws_all(obj, verbose=verbose)
    inhkws = sorted(list(set(allkws) - set(curkws)))
    return inhkws

def getattrs_cur(obj, use_lvl_vals: bool = False, verbose: bool = False) -> dict:
    attrs = getattrs_at_lvl(obj, 0, agg=True, use_lvl_vals=use_lvl_vals, verbose=verbose)
    return attrs

def getattrs_all(obj, use_lvl_vals: bool = False, verbose: bool = False) -> dict:
    attrs = getattrs_at_lvl(obj, float('inf'), agg=True, use_lvl_vals=use_lvl_vals, verbose=verbose)
    return attrs

def getattrs_inh(obj, use_lvl_vals: bool = False, verbose: bool = False) -> dict:
    curattrs = getattrs_cur(obj, use_lvl_vals, verbose)
    allattrs = getattrs_all(obj, use_lvl_vals, verbose)
    inhattrs = {k: v for k, v in allattrs.items() if k not in curattrs}
    return inhattrs

